Self-paced · 18 lessons · 5 parts

Model Intelligent Systems with COH and Implement with GISMOL

A hands-on tutorial from first principles to full systems. Learn the Constrained Object Hierarchies framework and build real intelligent systems with the GISMOL toolkit.

Time per lesson 45 – 90 min
Prerequisites Python 3.9+
Dependencies None required
Format Runnable code

00How to use this tutorial

This is a learning path, not a reference. Read it in order. Each lesson builds on the previous one, introduces one idea, and ends with exercises you should actually do before moving on.

Structure of every lesson

  • Learning objectives — what you will be able to do by the end
  • Concept — the idea, explained from first principles
  • Practice — runnable code that demonstrates the idea
  • Exercises — problems to solve before moving on
  • Key takeaways — three things to remember
Time budget: roughly 45–90 minutes per lesson. Reading the whole tutorial end to end takes about 12 hours, or five half-day sessions.

The five parts

PartThemeLessonsWhat you will do
IFoundations1–3Understand why COH exists
IIBuilding blocks4–8Build your first real COH systems
IIIMaking it smart9–12Add learning, dynamics, safety proofs
IVAdvanced composition13–16Compose and reason over systems
VMastery17–18Build a complete system

Setup

pip install -e ".[dev]"
python -c "import gismol; print(gismol.__version__)"

You should see 0.1.0.

Part I · Lessons 1–3

Foundations

Before you can build intelligent systems, you need to know what "intelligent system" means in this framework.

01The problem COH was built to solve

Learning objectives

  • State the four requirements an AGI formalism must satisfy
  • Explain why existing paradigms each fail at least one requirement
  • Describe what "constrained hierarchical optimisation" means

Concept

Imagine you are asked to design a general intelligence. Not a chatbot, not an image classifier — a system that could, in principle, do all of these and more, in a way that is safe, interpretable, and learnable.

Requirement 1 · Hierarchical composition. Intelligence is modular. A robot has joints, then limbs, then bodies, then behaviours. The formalism must support nested structure — and composing systems from smaller systems without losing the meaning of the parts.

Requirement 2 · Adaptive learning. No fixed set of rules covers the world. Intelligence must learn — but learning must be scoped: a robot should not have to retrain its whole policy to learn a new grip.

Requirement 3 · Constraint satisfaction. Some things must never happen. A pacemaker must not fire when the heart is beating normally. Constraints are not preferences — they are invariants.

Requirement 4 · Real-time monitoring. Constraints are useless if they are only checked at the end. Intelligence must continuously monitor and correct, the way homeostasis keeps a body's temperature within a narrow band.

Now look at the major paradigms:

ParadigmHierarchyLearningConstraintsMonitoring
Symbolic AI✓✗✓✗
Deep learningpartial✓✗✗
Hybrid / neurosymbolicpartial✓partial✗
Reinforcement learningpartial✓partial✗
COH✓✓✓✓
Intelligence is constrained hierarchical optimisation. Pursue goals, but stay within bounds. Everything in GISMOL serves that one sentence.

Practice

Let's see the four requirements in a tiny example — a system that keeps a value between 0 and 1, learns to reach a target, and monitors continuously:

from gismol import COHObject, Attribute
from gismol.core.constraints import IdentityConstraint, GoalConstraint
from gismol.core.daemons import InvariantDaemon, BoundaryDaemon
from gismol.neural.components import MLPComponent

system = COHObject("demo")

# Hierarchy: attributes are children of the object
system.add_attribute(Attribute("value", 0.5, bounds=(0.0, 1.0)))
system.add_attribute(Attribute("target", 0.8))

# Learning: a neural component that can be trained
system.add_neural(MLPComponent("controller", [1, 4, 1], seed=0))

# Constraint: value must stay in [0, 1]
system.add_identity_constraint(IdentityConstraint("bounded", "value <= 1.0"))

# Monitoring: a daemon enforces it every tick
system.add_daemon(InvariantDaemon(constraints=list(system.I)))
system.add_daemon(BoundaryDaemon({"value": (0.0, 1.0)}))

# Goal: get to the target
system.add_goal_constraint(GoalConstraint(
    "reach_target", lambda s: -abs(s["value"] - s["target"]), direction="max"
))

print(system.component_report())
# {'C': 0, 'A': 2, 'M': 0, 'N': 1, 'E': 'HashEmbedder',
#  'I': 1, 'T': 0, 'G': 1, 'D': 2}

Exercises

  1. List one intelligent system you use every day. Which of the four requirements does it satisfy?
  2. In your own words, why is "monitoring" separate from "constraint satisfaction"?
  3. Add a second identity constraint (value >= 0.0) and confirm the report shows two invariants.

Key takeaways

  • COH answers a specific question: what four properties must a general intelligence have?
  • Each existing paradigm fails at least one requirement. COH is a unification, not a competitor.
  • The central claim is one sentence. Intelligence is constrained hierarchical optimisation.

02Four intuitions behind COH

Learning objectives

  • Describe the four intuitions in plain English
  • Explain how each intuition maps to a component of the framework
  • Recognise these intuitions in real systems

Concept

COH is a synthesis. Each of its four key ideas was first discovered elsewhere.

Intuition 1 · Hierarchy is how the brain builds meaning. The cerebral cortex is arranged in layers. Hierarchical organisation is a general solution to the problem of building complex representations from simple parts. COH formalises this: every intelligent system is a hierarchy of components.

Intuition 2 · Constraints are priors. When you walk into a familiar room, you already expect gravity to point down and furniture to be solid. These expectations are priors. COH formalises this: identity constraints are structural priors.

Intuition 3 · Living systems maintain themselves. Your body temperature stays near 37°C even as the outside swings from 5°C to 40°C. COH formalises this: daemons are continuous monitors.

Intuition 4 · Composition is mathematically clean. If system A satisfies C₁ and system B satisfies C₂, then A × B satisfies both. This comes from category theory. COH formalises this: the class of COH objects forms a category.

IntuitionCOH componentGISMOL class
Hierarchy builds meaningHierarchical structure CCOHObject.C
Constraints are priorsIdentity constraints IIdentityConstraint
Systems maintain themselvesDaemons DDaemon subclasses
Composition is cleanThe category COHCOHCategory

Practice

from gismol import COHObject, Attribute, Method
from gismol.core.constraints import IdentityConstraint
from gismol.core.daemons import SafetyDaemon

pacemaker = COHObject("pacemaker")

# Intuition 1: hierarchy
heart = pacemaker.add_child(COHObject("heart"))
atrium = heart.add_child(COHObject("atrium"))
ventricle = heart.add_child(COHObject("ventricle"))

# Intuition 2: constraints are priors
pacemaker.add_identity_constraint(IdentityConstraint(
    "viable_rate", "heart_rate >= 40 and heart_rate <= 180"
))

# Intuition 3: homeostasis
pacemaker.add_daemon(SafetyDaemon(
    hazard=lambda s: abs(s["heart_rate"] - 70) - 30,
    threshold=0.0,
    intervention="adjust_pacing",
))

# Intuition 4: composition
combined = pacemaker.product(COHObject("monitor"))
print(f"Combined system has {len(combined.C)} components")

Exercises

  1. Add a trigger that fires when the rate drops below 50.
  2. Explain why a pacemaker needs all four intuitions — not just constraints.
  3. Look at the warehouse fleet example. Identify lines corresponding to each intuition.

Key takeaways

  • COH is a synthesis. Four ideas from four fields.
  • Each intuition maps to a component.
  • The 9-tuple is minimal. Exactly what is needed to express all four.

03The 9-tuple tour

Learning objectives

  • Name and describe each of the nine components
  • Explain why each is orthogonal
  • Read the component_report() of any COH object

Concept

Every intelligent system in COH is a nine-part structure:

O = (C, A, M, N, E, I, T, G, D)

Take a thermostat. Here is how it decomposes:

SymbolComponentThermostat's version
CHierarchyHouse contains rooms; each room has a thermostat
AAttributestemperature, target, humidity
MMethodsheat(), cool(), turn_off()
NNeural componentsA learned predictor of temperature drift
EEmbeddingA vector representing this thermostat's state
IIdentity constraintstemperature <= 100, temperature >= -40
TTriggersIf temp drops below target, start heating
GGoalsKeep temperature near target, minimise energy
DDaemonsContinuous monitors ensuring safety limits

The nine components are orthogonal — none can be reduced to another. C is not A (a value vs. a system). A is not M (measure vs. transform). M is not T (what vs. when). N is not M (fixed vs. parameterised). I, T, G cannot be reduced to each other (always true vs. event-driven vs. optimisation). D is not I (process vs. predicate).

Practice

from examples.systems_demo import build_warehouse_fleet

fleet = build_warehouse_fleet()
for symbol, count in fleet.component_report().items():
    print(f"{symbol}: {count}")

# C: 3
# A: 3
# M: 1
# N: 1
# E: FeatureEmbedder
# I: 2
# T: 2
# G: 3
# D: 2

Every component is populated. The fleet is a complete COH object.

Exercises

  1. Take three systems from examples/systems_demo.py. List every component in each.
  2. Choose one component and explain why it cannot be described in terms of the other eight.
  3. Build an empty COH object. Predict its component_report().

Key takeaways

  • The 9-tuple is a checklist. Every component answers a distinct question.
  • Components are orthogonal. None can be reduced to another.
  • Minimality is a theorem, not a philosophical claim.
Part II · Lessons 4–8

Building blocks

Now that you know what the components mean, this part teaches you to build systems with them.

04Objects and hierarchies (C)

Learning objectives

  • Create COHObject instances and attach children
  • Traverse a hierarchy in all directions
  • Understand cycle detection and topological ordering

Concept

Every intelligent system is a COHObject. A COHObject can contain other COHObjects as children, to arbitrary depth. The result is a directed acyclic graph (DAG).

A flat system with 100 attributes is hard to reason about. A hierarchical system with 5 groups of 20 attributes each doing one thing is tractable. Hierarchy is what makes complexity manageable.

If A contains B and B contains A, then "A's state" is undefined. Cycles break the semantics. GISMOL detects them at construction time.

Practice

from gismol import COHObject

arm = COHObject("arm")
shoulder = arm.add_child(COHObject("shoulder"))
elbow = arm.add_child(COHObject("elbow"))
wrist = arm.add_child(COHObject("wrist"))

shoulder.add_child(COHObject("motor"))
wrist.add_child(COHObject("gripper"))

print([o.name for o in arm.descendants()])
# ['shoulder', 'motor', 'elbow', 'wrist', 'gripper']

print([o.name for o in arm.topological_order()])
# ['motor', 'shoulder', 'elbow', 'gripper', 'wrist', 'arm']

Now try to make a cycle:

a = COHObject("a")
b = COHObject("b")
a.add_child(b)
try:
    b.add_child(a)
except Exception as e:
    print(f"Caught: {type(e).__name__}: {e}")
# Caught: CyclicHierarchyError: cyclic component hierarchy detected: b -> a
GISMOL refuses to build ill-founded structures. Acyclicity is enforced at construction time — cycles never sneak into a running system.

Exercises

  1. Build a hierarchy for a drone: body (motors, battery) and payload (camera, gimbal).
  2. Add a child after the fact and confirm the DAG updates.
  3. Try to make a three-level cycle (A → B → C → A).

Key takeaways

  • Every system is a COHObject.
  • Acyclicity is enforced at construction.
  • Topological order gives you bottom-up evaluation for free.

05State and attributes (A)

Learning objectives

  • Declare attributes with types, bounds, and descriptions
  • Read and write nested state with dotted paths
  • Distinguish the initial state from the current state

Concept

An attribute is a measurable state variable — something the system can observe, measure, or record. Physical quantities, logical flags, counters, latent quantities.

Attributes live in a State, a dict-like object supporting dotted paths. If arm has a child joint with attribute angle, you can access it as "joint.angle".

Practice

from gismol import COHObject, Attribute

thermostat = COHObject("thermostat")
thermostat.add_attribute(Attribute(
    "temperature", 20.0,
    bounds=(0.0, 60.0),
    description="Room temperature",
    unit="°C",
))
thermostat.add_attribute(Attribute("target", 22.0))
thermostat.add_attribute(Attribute("mode", "off", space="categorical"))

print(thermostat.state)
# State({'temperature': 20.0, 'target': 22.0, 'mode': 'off'})

thermostat.set("temperature", 25.5)
print(thermostat.get("temperature"))    # 25.5

# Nested state via children
house = COHObject("house")
house.add_attribute(Attribute("outdoor_temp", 10.0))
house.add_child(thermostat)

print(house.get("thermostat.temperature"))   # 25.5
house.set("thermostat.target", 21.0)

Exercises

  1. Add a humidity attribute with bounds (0, 100).
  2. Build a two-room house. Verify independence.
  3. Change a nested attribute and confirm get() returns the new value.

Key takeaways

  • Attributes describe what the system measures.
  • State supports dotted paths natively.
  • Bounds are for daemons, not validation.

06Actions and methods (M)

Learning objectives

  • Wrap functions as Method objects
  • Write methods with pre- and post-conditions
  • Invoke methods directly and through triggers

Concept

A method is an executable transformation. It takes the current state and produces a new state, with an optional reward.

Why wrap methods in a Method class? Contracts (pre- and post-conditions), discovery (triggers refer by name), instrumentation (every call is counted), serialisation (metadata is stored).

Purity. A method should not mutate the input state. Return a new state instead.

Practice

from gismol import COHObject, Attribute, Method

robot = COHObject("robot")
robot.add_attribute(Attribute("x", 0.0, bounds=(-100, 100)))
robot.add_attribute(Attribute("energy", 1.0, bounds=(0.0, 1.0)))
robot.add_attribute(Attribute("holding", False, space="bool"))

def move_east(state, action=None):
    new = state.copy()
    new["x"] += 1.0
    new["energy"] = max(0.0, new["energy"] - 0.02)
    return new, 0.1

def pick_up(state, action=None):
    new = state.copy()
    new["holding"] = True
    return new, 1.0

robot.add_method(Method(
    "move_east", move_east,
    description="Move one unit east; costs energy",
))
robot.add_method(Method(
    "pick_up", pick_up,
    preconditions=[lambda s: not s["holding"]],
    postconditions=[lambda s: s["holding"] is True],
))

new_state, reward = robot.invoke("move_east")
print(f"After move: x={new_state['x']}, reward={reward}")

robot.invoke("pick_up")
try:
    robot.invoke("pick_up")   # precondition fails
except Exception as e:
    print(f"Caught: {type(e).__name__}")

Exercises

  1. Add a rest method that restores energy to 1.0.
  2. Add a move_north symmetric to move_east.
  3. Write a postcondition that x >= 0 and try to violate it.

Key takeaways

  • Methods are actions with contracts.
  • Purity is a discipline, not an enforcement.
  • Rewards are optional.

07Constraints — the heart of COH (I, T, G)

Learning objectives

  • Write identity constraints, triggers, and goals
  • Understand the difference between them
  • Use the safe expression language

Concept

There are three types of constraint, each with a distinct logical status:

TypeLogical formMeaning
Identity (I)∀s. admissible(s) → φ(s)Always true in admissible states
Trigger (T)event ∧ φ(s) → actionReacts to events
Goal (G)maximise/minimise g(s)Optimises a score

A crucial distinction: identity constraints are hard. Triggers and goals are soft. You cannot "partially" satisfy an identity constraint — either the state is admissible or it is not. This is what makes safety verifiable.

The safe expression language

Three ways to write a constraint:

# (a) Lambda
IdentityConstraint("positive", lambda s: s["x"] > 0)

# (b) Expression tree
IdentityConstraint("positive", E.Var("x") > 0)

# (c) String — parsed safely, no eval
IdentityConstraint("positive", "x > 0")

Practice

from gismol import COHObject, Attribute
from gismol.core.constraints import (
    IdentityConstraint, TriggerConstraint, GoalConstraint,
)

reactor = COHObject("reactor")
reactor.add_attribute(Attribute("temperature", 20.0, bounds=(0.0, 1000.0)))
reactor.add_attribute(Attribute("coolant_flow", 5.0, bounds=(0.0, 20.0)))
reactor.add_attribute(Attribute("power_output", 0.0, bounds=(0.0, 100.0)))

# Invariants
reactor.add_identity_constraint(IdentityConstraint(
    "thermal_limit", "temperature < 900",
    description="Core must never exceed 900°C",
))

# Triggers
reactor.add_trigger_constraint(TriggerConstraint(
    "overheat_response", "thermal_high",
    condition="temperature > 800",
    action="increase_coolant",
    priority=5,
))

# Goals
reactor.add_goal_constraint(GoalConstraint(
    "max_output", "power_output",
    direction="max", weight=3.0,
))

print(reactor.constraints.summary())

Exercises

  1. Add an invariant: power_output <= coolant_flow * 10.
  2. Add a trigger that fires on "emergency" and sets coolant flow to 20.
  3. Tune the goal weights and observe changes.

Key takeaways

  • Identity constraints are hard; triggers and goals are soft.
  • The constraint language is safe — no eval.
  • Multi-objective optimisation comes free.

08Daemons — real-time enforcement (D)

Learning objectives

  • Explain what a daemon is and why it is separate from a constraint
  • Use the built-in daemons
  • Write a custom daemon

Concept

A daemon is a process, not a predicate. It runs continuously, observes state, and emits corrective actions. A constraint describes what is true. A daemon makes it true.

Built-in daemons

DaemonPurpose
InvariantDaemonReports identity violations
BoundaryDaemonProjects state into a box
RateDaemonLimits rate of change
SafetyDaemonHazard-margin monitoring
HomeostasisDaemonSet-point regulation
BarrierDaemonControl-barrier forward invariance
PrecisionWeightedDaemonPrediction-error minimisation
DaemonNetworkComposite; highest severity wins

Practice

from gismol import COHObject, Attribute
from gismol.core.constraints import IdentityConstraint
from gismol.core.daemons import (
    InvariantDaemon, BoundaryDaemon, RateDaemon,
)
from gismol.core.daemon_impl import HomeostasisDaemon, BarrierDaemon

reactor = COHObject("reactor")
reactor.add_attribute(Attribute("temperature", 25.0, bounds=(0.0, 1000.0)))
reactor.add_attribute(Attribute("power", 50.0, bounds=(0.0, 100.0)))
reactor.add_identity_constraint(IdentityConstraint("t_limit", "temperature < 900"))

reactor.add_daemon(InvariantDaemon(constraints=list(reactor.I)))
reactor.add_daemon(BoundaryDaemon({
    "temperature": (0.0, 900.0),
    "power": (0.0, 100.0),
}))
reactor.add_daemon(RateDaemon("temperature", max_rate=50.0))
reactor.add_daemon(HomeostasisDaemon("temperature", setpoint=500.0, gain=0.1))
reactor.add_daemon(BarrierDaemon(
    barrier=lambda s: 850.0 - s["temperature"],
    alpha=lambda h: 0.5 * h,
    intervention={"power": 0.0},
))

print(f"Daemon network size: {len(reactor.D)}")

Writing a custom daemon

from gismol.core.daemons import Daemon, CorrectiveAction

class ThermalThrottleDaemon(Daemon):
    """Reduce power when approaching the thermal limit."""

    def __init__(self, limit=850.0, margin=100.0):
        super().__init__("thermal_throttle")
        self.limit = limit
        self.margin = margin

    def observe(self, state, t=0.0):
        temp = state.get("temperature", 0.0)
        if temp < self.limit - self.margin:
            return None
        excess = (temp - (self.limit - self.margin)) / self.margin
        throttle = max(0.0, 1.0 - excess)
        return self._emit(CorrectiveAction(
            name="throttle",
            payload={"power_limit": throttle},
            daemon=self.name,
            severity=excess,
            message=f"temp={temp:.1f} → throttle={throttle:.2f}",
        ))

reactor.add_daemon(ThermalThrottleDaemon(limit=850.0, margin=100.0))

Exercises

  1. Attach a daemon that caps power at 80 when it exceeds 80.
  2. Write a daemon that fires only on alternating ticks.
  3. Attach a barrier daemon with a quadratic barrier.

Key takeaways

  • Daemons make constraints real.
  • The network arbitrates by severity.
  • Custom daemons are easy — subclass, implement observe.
Part III · Lessons 9–12

Making it smart

Your systems are now structurally sound. This part adds representation, learning, and dynamics.

09Embeddings — giving objects meaning (E)

Learning objectives

  • Explain what a Hilbert-space embedding is for
  • Choose the right embedder for a task
  • Use embeddings for similarity and clustering

Concept

An embedding is a map from objects to vectors:

E : O → H

where H is a Hilbert space. This gives every COH object a representation that supports similarity, distance, clustering, and retrieval.

Practice

from gismol import COHObject, Attribute
from gismol.core.embedding import HashEmbedder, FeatureEmbedder

def build_pump(flow, temp):
    p = COHObject("pump")
    p.add_attribute(Attribute("flow", flow, bounds=(0.0, 100.0)))
    p.add_attribute(Attribute("temperature", temp, bounds=(0.0, 200.0)))
    return p

a = build_pump(50.0, 60.0)
b = build_pump(52.0, 62.0)
c = build_pump(10.0, 150.0)

# Feature embedder — attribute-based
emb = FeatureEmbedder(
    ["flow", "temperature"],
    ranges={"flow": (0, 100), "temperature": (0, 200)},
)
a.set_embedder(emb)
b.set_embedder(emb)
c.set_embedder(emb)

print(f"a ~ b: {a.similarity(b):.4f}")   # ~0.9999
print(f"a ~ c: {a.similarity(c):.4f}")   # ~0.2161

Exercises

  1. Build three systems differing only in the number of children. Compare their hash similarities.
  2. Train a NeuralEmbedder and confirm similar inputs map to similar outputs.
  3. Cluster 20 systems into two groups using a SimilarityIndex.

Key takeaways

  • Embeddings turn systems into vectors.
  • Choose the embedder based on what you want to compare.
  • Similarity is a first-class operation.

10Neural components — learning inside structure (N)

Learning objectives

  • Add a neural component to a COHObject
  • Train it on data
  • Use policy, value, and predictor components

Concept

Neural components are parameterised adaptive functions:

N = { f_θ : X → Y  |  θ ∈ Θ }

Learning is scoped. A perception component extracts features. A policy component learns what to do. Each lives inside the object that needs it and inherits that object's constraints.

Practice

import random
from gismol import COHObject, Attribute
from gismol.neural.components import MLPComponent
from gismol.neural.optimizers import Adam

thermostat = COHObject("thermostat")
thermostat.add_attribute(Attribute("temperature", 20.0))
thermostat.add_attribute(Attribute("heater_power", 0.0))
thermostat.add_attribute(Attribute("outdoor_temp", 10.0))

predictor = MLPComponent("next_temp", [3, 16, 1], seed=42)
thermostat.add_neural(predictor, name="predictor")

def simulate(T, P, O):
    return T + 0.1 * P * (25.0 - T) - 0.05 * (T - O)

X, Y = [], []
for _ in range(1000):
    T = random.uniform(10.0, 30.0)
    P = random.uniform(0.0, 1.0)
    O = random.uniform(-5.0, 20.0)
    X.append([T, P, O])
    Y.append([simulate(T, P, O)])

history = predictor.fit(
    X, Y, epochs=200, lr=0.05,
    optimizer=Adam(0.02), verbose=False,
)
print(f"Loss: {history[0]:.4f} → {history[-1]:.6f}")

Training with constrained RL

from gismol.neural.constrained_rl import ConstrainedMDP, LagrangianAgent

env = ConstrainedMDP(n_states=4, n_actions=2,
                     cost_limit=0.5, horizon=10, seed=0)
agent = LagrangianAgent(env, seed=0)
history = agent.train(iterations=100, episodes_per_iter=8, verbose=False)
print(f"Return: {history[-1]['return']:.3f}")
print(f"Cost:   {history[-1]['cost']:.3f}")

Exercises

  1. Train a ValueComponent to predict discounted return.
  2. Save and reload the trained theta.
  3. Train a PolicyComponent on a two-armed bandit.

Key takeaways

  • Neural components are local and inherit constraints.
  • Training is standard gradient descent — no external framework needed.
  • Constrained RL uses primal-dual.

11Hybrid dynamics — simulation

Learning objectives

  • Attach a continuous flow to a system
  • Run a simulation and interpret the trajectory
  • Use events to trigger discrete transitions

Concept

A COH system is hybrid: it combines

  • Continuous evolution — ds/dt = f(s, a, θ)
  • Discrete jumps — s_{t+1} = g(s_t, a_t, θ)
  • Switching conditions — triggers

Practice

from gismol import COHObject, Attribute
from gismol.core.dynamics import ContinuousFlow
from gismol.core.constraints import GoalConstraint

tank = COHObject("cooling_tank")
tank.add_attribute(Attribute("temperature", 80.0, bounds=(0.0, 200.0)))
tank.add_attribute(Attribute("ambient", 20.0))

tank.flow = ContinuousFlow(
    f=lambda s, a, th: {
        "temperature": -0.1 * (s["temperature"] - s["ambient"]),
    },
    variables=["temperature"],
)

tank.add_goal_constraint(GoalConstraint(
    "reach_ambient",
    lambda s: -abs(s["temperature"] - s["ambient"]),
    direction="max",
))

traj = tank.simulate(steps=100, dt=0.1)
print(f"Initial: {traj.states[0]['temperature']:.2f}")
print(f"Final:   {traj.final['temperature']:.2f}")

Exercises

  1. Attach a flow with two coupled variables.
  2. Add a trigger that fires on "temp_high" and drops the temperature.
  3. Try different dt values and observe accuracy.

Key takeaways

  • Continuous and discrete evolution are both first-class.
  • Events drive discrete jumps.
  • RK4 gives accurate integration.

12Stability and safety proofs

Learning objectives

  • Explain what a Lyapunov function is
  • Build one from constraints or goals
  • Interpret a stability certificate

Concept

A Lyapunov function is a scalar V(s) ≥ 0 that decreases along trajectories. If one exists inside the admissible region, the system is stable.

For COH systems, the natural Lyapunov function is the sum of constraint violations:

V_constraints(s) = Σ_i violation_i(s)

Practice

import random
from gismol.stability.lyapunov import (
    ConstraintStabilityAnalyzer, lyapunov_from_constraints,
)

tank = build_cooling_tank()   # from Lesson 11
V = lyapunov_from_constraints(tank)

samples = [
    {"temperature": random.uniform(0.0, 200.0), "ambient": 20.0}
    for _ in range(100)
]

analyzer = ConstraintStabilityAnalyzer(tank)
cert = analyzer.analyze(V, states=samples)

print(f"Stable:         {cert.stable}")
print(f"Asymptotically: {cert.asymptotic}")
print(f"Decay rate:     {cert.rate:.4f}")

Exercises

  1. Attach a restoring flow and verify stability.
  2. Take a system with two goals of opposite sign. Is it stable?
  3. Build a system stable inside a bounded region but unstable outside.

Key takeaways

  • Stability is provable, not just testable.
  • COH provides natural Lyapunov functions.
  • Instability is diagnosable.
Part IV · Lessons 13–16

Advanced composition

Compose systems from other systems, learn under constraints, and reason over them in natural language.

13Category theory composition

Learning objectives

  • Compose two systems as a product
  • Use coproducts to model alternative behaviours
  • Verify that a transformation preserves structure

Concept

The class of COH objects forms a category. Three fundamental compositions:

  • Product O₁ × O₂ — parallel composition
  • Coproduct O₁ + O₂ — alternative composition
  • Exponential O₂^O₁ — function objects

Practice

from gismol import COHObject, Attribute
from gismol.categorical import COHMorphism

sensor = COHObject("sensor")
sensor.add_attribute(Attribute("reading", 0.0))

actuator = COHObject("actuator")
actuator.add_attribute(Attribute("output", 0.0))

combined = sensor.product(actuator)
print(f"Product children: {list(combined.C)}")

either = sensor.coproduct(actuator)
transformer = actuator.exponential(sensor)

small = COHObject("small", attributes=[Attribute("x", 1.0)])
large = COHObject("large", attributes=[Attribute("x", 1.0)])
mor = COHMorphism(source=small, target=large, name="upgrade")
print(f"Preserves: {mor.preserves()}")

Exercises

  1. Build a robot with two arms using product().
  2. Model a switching controller as a coproduct.
  3. Build a system that takes another system as input.

Key takeaways

  • COH objects compose cleanly.
  • Composition respects constraints.
  • Structure-preserving maps are verified.

14Learning under constraints

Learning objectives

  • Formulate a constrained learning problem
  • Use primal-dual optimisation
  • Balance reward and safety

Concept

Standard RL maximises reward. Constrained RL solves:

max_π  E[G(τ)]   s.t.  E[C_i(τ)] ≤ 0   ∀i

The solution uses Lagrangian duality: introduce multipliers, alternate between updating the policy and updating the multipliers. This is primal-dual optimisation.

Practice

from gismol.neural.constrained_rl import ConstrainedMDP, LagrangianAgent

env = ConstrainedMDP(
    n_states=6, n_actions=3,
    cost_limit=0.4, horizon=15, seed=42,
)
agent = LagrangianAgent(env, lr_theta=0.05, lr_lambda=0.1, seed=0)

history = agent.train(iterations=200, episodes_per_iter=8, verbose=False)

for i in [0, 50, 100, 150, 199]:
    h = history[i]
    print(f"iter {i:3d}: return={h['return']:+.3f}  "
          f"cost={h['cost']:.3f}  lambda={h['lambda']:.3f}")

Exercises

  1. Change cost_limit to 0.2 and retrain.
  2. Add a second constraint and observe the Lagrangian scaling.
  3. Try a very high lr_lambda.

Key takeaways

  • Constrained learning is a trade-off.
  • The Lagrangian multiplier is interpretable.
  • Primal-dual converges under convexity.

15The natural language interface

Learning objectives

  • Compile English text into a COH object
  • Use the chat pipeline
  • Recognise the pipeline's strengths and limits

Concept

The NLP front-end is a compiler: text → normalise → parse → extract entities → mine relations → recognise intent → compile constraints → attach defaults → build.

The pipeline is rule-based, not ML-based. For critical systems, build programmatically and use NLP only for the outer, flexible parts.

Practice

from gismol.nlp import text_to_coh, ChatModule

text = """
Define a reactor called R1.
The core temperature must be less than 900.
If temperature exceeds 800 then increase coolant.
Maximize energy output.
"""

reactor = text_to_coh(text, name="R1")
print(reactor.describe())

chat = ChatModule()
print(chat.process("define a system called pump").text)
print(chat.process("the pressure must be less than 200").text)
print(chat.process("maximize throughput").text)

Exercises

  1. Write a description of your own system. Compile it.
  2. Add a custom entity pattern.
  3. Add a custom relation pattern.

Key takeaways

  • NLP is a compiler, not magic.
  • The chat pipeline is stateful.
  • Always inspect the extracted metadata.

16Reasoners

Learning objectives

  • Query a COH object with different reasoners
  • Diagnose constraint violations
  • Plan actions with the goal solver

Concept

ReasonerAnswers
ConstraintReasonerIs this state admissible? How to repair?
GoalSolverWhat method sequence maximises goals?
ForwardChainerWhat facts follow from the current state?
AbductiveReasonerWhat would explain this violation?
CausalReasonerWhat causes what?
AnalogicalReasonerHow similar are these systems?
DomainReasonerHow does this behave under domain rules?
MetaReasonerWhich reasoner should I use?

GeneralReasoner classifies the query and dispatches.

Practice

from gismol import COHObject, Attribute, Method
from gismol.core.constraints import IdentityConstraint, GoalConstraint
from gismol.reasoners import (
    ConstraintReasoner, GoalSolver, GeneralReasoner, Query,
)

pump = COHObject("pump")
pump.add_attribute(Attribute("pressure", 100.0, bounds=(0.0, 300.0)))
pump.add_attribute(Attribute("target_pressure", 150.0))
pump.add_method(Method("increase", lambda s, a=None: ({
    **s, "pressure": min(300.0, s["pressure"] + 10.0),
}, 0.0)))
pump.add_identity_constraint(IdentityConstraint("p_limit", "pressure < 250"))
pump.add_goal_constraint(GoalConstraint(
    "target", lambda s: -abs(s["pressure"] - s["target_pressure"]),
    direction="max",
))

result = ConstraintReasoner().infer(pump, Query("check invariants"))
print(f"All OK? {result.answer is True}")

pump.set("pressure", 300.0)
result = ConstraintReasoner().infer(pump, Query("violations"))
print(f"Repairs: {result.answer['repairs']}")

result = GoalSolver(depth=3, beam=4).infer(pump, Query("best plan"))
print(f"Plan: {result.answer['plan']}")

Exercises

  1. Add a method and re-run the goal solver.
  2. Query the causal reasoner with an intervention.
  3. Write a custom Rule and attach it to a ForwardChainer.

Key takeaways

  • Different reasoners answer different questions.
  • Every result carries a trace.
  • Reasoners use the same constraints, methods, and goals.
Part V · Lessons 17–18

Mastery

You know the pieces. This part puts them together into a full system.

17A complete system

Learning objectives

  • Build a five-layer system that exercises every component
  • Validate it thoroughly
  • Simulate, reason, and analyse it

Concept

We build a water treatment plant — five levels, all nine components, real safety requirements.

water_plant                   (level 0)
├── intake_system             (level 1)
│   ├── pump_station          (level 2)
│   │   ├── pump_1            (level 3)
│   │   │   └── motor_control (level 4)
│   │   └── pump_2
│   └── quality_sensor
├── treatment_system
│   ├── filtration
│   └── disinfection
└── distribution_system

Practice

from gismol import COHObject, Attribute, Method
from gismol.core.constraints import (
    IdentityConstraint, TriggerConstraint, GoalConstraint,
)
from gismol.core.daemons import (
    InvariantDaemon, BoundaryDaemon, RateDaemon,
)
from gismol.core.daemon_impl import HomeostasisDaemon, BarrierDaemon
from gismol.core.embedding import FeatureEmbedder
from gismol.neural.components import MLPComponent
from gismol.nlp.validation import COHValidator

def motor_control(name):
    m = COHObject(name)
    m.add_attribute(Attribute("rpm", 0.0, bounds=(0.0, 3600.0)))
    m.add_attribute(Attribute("temperature", 25.0, bounds=(-10.0, 90.0)))
    m.add_identity_constraint(IdentityConstraint("thermal_safe", "temperature < 80"))
    m.add_daemon(InvariantDaemon(constraints=list(m.I)))
    m.add_daemon(BoundaryDaemon({
        "rpm": (0.0, 3600.0),
        "temperature": (-10.0, 80.0),
    }))
    m.add_daemon(HomeostasisDaemon("temperature", setpoint=25.0, gain=0.1))
    m.add_neural(MLPComponent("rpm_ctrl", [3, 8, 1], seed=1), name="controller")
    return m

def pump(name, seed):
    p = COHObject(name)
    p.add_attribute(Attribute("flow_lpm", 0.0, bounds=(0.0, 500.0)))
    p.add_attribute(Attribute("pressure_bar", 0.0, bounds=(0.0, 10.0)))
    p.add_child(motor_control(f"{name}_motor"))
    p.add_identity_constraint(IdentityConstraint(
        "pressure_safe", "pressure_bar < 8"))
    p.add_trigger_constraint(TriggerConstraint(
        "overpressure", "pressure_high", "pressure_bar > 6",
        {"pressure_bar": 5.0}, priority=9))
    p.add_goal_constraint(GoalConstraint(
        "meet_demand", "flow_lpm", direction="max", weight=2.0))
    p.add_daemon(InvariantDaemon(constraints=list(p.I)))
    p.add_daemon(BarrierDaemon(
        barrier=lambda s: 8.0 - s["pressure_bar"],
        alpha=lambda h: 2.0 * h,
    ))
    p.set_embedder(FeatureEmbedder(["flow_lpm", "pressure_bar"]))
    return p

# Assemble the plant
plant = COHObject("water_plant", description="Municipal water treatment facility")
plant.add_attribute(Attribute("total_production_m3h", 0.0, bounds=(0.0, 1000.0)))
plant.add_attribute(Attribute("energy_kwh", 0.0, bounds=(0.0, 10000.0)))
plant.add_attribute(Attribute("quality_score", 1.0, bounds=(0.0, 1.0)))
plant.add_child(pump("pump_1", seed=10), key="pump_1")
plant.add_child(pump("pump_2", seed=11), key="pump_2")
plant.add_identity_constraint(IdentityConstraint(
    "quality_maintained", "quality_score > 0.85"))
plant.add_daemon(InvariantDaemon(constraints=list(plant.I)))

# Validate
validator = COHValidator()
errors = [i for i in validator.validate(plant) if i.severity == "error"]
print(f"Validator errors: {len(errors)}")
print(f"Sound: {plant.soundness_report()['sound']}")
print(f"Component report: {plant.component_report()}")

What you should observe

  1. The hierarchy goes five levels deep.
  2. Every component is populated — nine out of nine.
  3. No validator errors.
  4. Simulation proceeds without violations.
  5. The reasoner provides a plan.

Exercises

  1. Change the target pressure and observe the homeostasis daemon.
  2. Remove the barrier daemon and see if the flow diverges.
  3. Find the two most similar subsystems by embedding.

Key takeaways

  • You can now build real systems.
  • Composition is the key.
  • Validation is not optional.

18Where to go next

Choose your path

You now know the theory, the framework, and the toolkit. Where you go next depends on what you want to do.

If you want to build applications

  • Robotics — a robot arm, a drone, a mobile manipulator
  • Control systems — a cruise controller, a HVAC system, a chemical process
  • Healthcare — a medication reminder, a triage assistant
  • Finance — a portfolio rebalancer, an execution algorithm, a risk monitor
  • Energy — a microgrid controller, a demand-response system
  • Education — an adaptive tutor, a curriculum planner

Common traps to avoid

  1. Over-constraining. Constraints should be necessary.
  2. Under-monitoring. Constraints without daemons are documentation.
  3. Neural without supervision. Pair every neural component with a constraint.
  4. Deep hierarchies for shallow problems. Three to five levels is right.
  5. Ignoring the embedder. Every system should have one.
  6. Skipping validation. Run soundness_report in CI.
  7. Confusing goals with constraints. Use the right type.
  8. Abandoning the theory. When your system misbehaves, look at the hierarchy.

Closing thought

You now understand that intelligence is constrained hierarchical optimisation. Every system you build with GISMOL is an instance of that idea.

Build well. Stay within bounds. Pursue your goals.
Appendix A

Learning path summary

PartLessonsKey skill
I1–3Understand why COH exists
II4–8Build hierarchies, state, constraints, daemons
III9–12Embeddings, neural components, dynamics, stability
IV13–16Compose, learn under constraints, NLP, reason
V17–18Build a complete system
Appendix B

Code cheat sheet

# ---- Build a COHObject ----
from gismol import COHObject, Attribute, Method
from gismol.core.constraints import (
    IdentityConstraint, TriggerConstraint, GoalConstraint,
)
from gismol.core.daemons import InvariantDaemon, BoundaryDaemon

coh = COHObject("name")
coh.add_attribute(Attribute("x", 0.0, bounds=(0.0, 10.0)))
coh.add_method(Method("do", lambda s, a=None: (s, 0.0)))
coh.add_identity_constraint(IdentityConstraint("i", "x >= 0"))
coh.add_trigger_constraint(TriggerConstraint("t", "evt", "x > 5", "do"))
coh.add_goal_constraint(GoalConstraint("g", "x", direction="max"))
coh.add_daemon(InvariantDaemon(constraints=list(coh.I)))
coh.add_daemon(BoundaryDaemon({"x": (0.0, 10.0)}))

# ---- Learning ----
from gismol.neural.components import MLPComponent
coh.add_neural(MLPComponent("nn", [3, 8, 1], seed=0))

# ---- Embedding ----
from gismol.core.embedding import FeatureEmbedder
coh.set_embedder(FeatureEmbedder(["x"]))

# ---- Dynamics ----
from gismol.core.dynamics import ContinuousFlow
coh.flow = ContinuousFlow(
    f=lambda s, a, th: {"x": -0.1 * s["x"]},
    variables=["x"],
)

# ---- Simulate, validate, reason ----
traj = coh.simulate(steps=100, dt=0.1)
print(coh.component_report())
print(coh.soundness_report())

from gismol.reasoners import GeneralReasoner, Query
GeneralReasoner().infer(coh, Query("maximize x"))

# ---- Compose ----
combined = coh1.product(coh2)

# ---- Stability ----
from gismol.stability.lyapunov import ConstraintStabilityAnalyzer
ConstraintStabilityAnalyzer(coh).analyze()

# ---- NLP ----
from gismol.nlp import text_to_coh
new_coh = text_to_coh("The temperature must be less than 100.")
Appendix C

Glossary

TermMeaning
Admissible regionStates where all identity constraints hold
Attribute (A)A measurable state variable
Barrier functionScalar whose non-negativity certifies safety
COHConstrained Object Hierarchy
CoproductCategorical alternative composition
Daemon (D)Continuous monitor enforcing invariants
Embedding (E)Map from objects to vectors
ExponentialCategorical function object
Free energyBound on surprise in variational inference
Goal constraint (G)Optimisation objective
Hybrid systemContinuous and discrete dynamics combined
Identity constraint (I)Invariant predicate
Lyapunov functionScalar decreasing along trajectories
Method (M)Executable state transformation
MorphismStructure-preserving map
Neural component (N)Parameterised adaptive function
ProductCategorical parallel composition
SoundnessInternal consistency
StateHierarchical value assignment
Trigger constraint (T)Event-condition-action rule
9-tuple(C, A, M, N, E, I, T, G, D)
👤