Skip to content

Lesson 8: Branches and OR Logic

The Python instinct

if auto_mode and state == "sorting" and is_large:
    diverter = True
elif manual_mode and diverter_button:
    diverter = True

The ladder logic way

Ladder logic has two ways to combine conditions. Commas inside Rung(...) are implicit AND. For OR, use Or():

from pyrung import Bool, Int, Program, Rung, branch, comment, out, latch, reset, Or, And

Auto          = Bool("Auto")
Manual        = Bool("Manual")
StopBtn       = Bool("StopBtn")     # NC contact
StartBtn      = Bool("StartBtn")
EstopOK       = Bool("EstopOK")     # NC safety relay permission
Running       = Bool("Running")
Light         = Bool("Light")
DiverterBtn   = Bool("DiverterBtn")
DiverterCmd   = Bool("DiverterCmd")
ConveyorMotor = Bool("ConveyorMotor")
StatusLight   = Bool("StatusLight")
Mode          = Int("Mode")

with Program() as logic:
    # Motor runs in either mode when started
    with Rung(Or(Auto, Manual)):
        out(Light)                        # Status light: either mode is active

    # Or works with comparisons and any number of conditions
    with Rung(Or(Mode == 1, Mode == 3, Mode == 5)):
        latch(Running)

Or is variadic — pass any number of conditions. And is the explicit form of comma-separated conditions, useful inside Or for nested groups. Mirrors a familiar Python pattern: any([a, b, c]) and all([a, b, c]).

Branches

A branch creates a parallel path within a rung. Think of it as a second wire that ANDs its condition with the parent's.

Here's the conveyor's motor rung. EstopOK gates everything — it's a permission input from the safety relay, True when the world is safe to run. Below that, the motor and status light share the same gate:

  EstopOK (parent rung — True when safe)
      +-- Running -> out(ConveyorMotor)
      +-- Running -> out(StatusLight)
with Program() as logic:
    comment("Start/stop — NC stop resets when pressed or wire broken")
    with Rung(StartBtn, Or(Auto, Manual)):
        latch(Running)
    with Rung(~StopBtn):
        reset(Running)
    with Rung(~EstopOK):
        reset(Running)

    comment("Motor output — EstopOK gates all outputs")
    with Rung(EstopOK):
        with branch(Running):
            out(ConveyorMotor)
        with branch(Running):
            out(StatusLight)

This is the gate pattern. The parent rung holds your master condition, and every branch inside inherits that permission automatically. Lose the gate, lose all the outputs — atomically, in one scan. EstopOK reads as "safety is satisfied" so the gate uses the raw tag with no ~. The reset rungs use ~StopBtn and ~EstopOK because those fire when the NC circuits open — same ~ convention from Lesson 3.

The gate pattern is the textbook ladder structure for any permission or interlock — guard doors, light curtains, machine-enabled flags. Real fail-safe E-stop wiring lives in Lesson 11; here, the gate is general-purpose.

Combining branches and Or

The diverter needs to fire in two cases: auto mode during sorting, or manual mode with the button pressed. That's Or with And — same pattern as any([all([...]), all([...]]) in Python:

    comment("Diverter output — auto sort OR manual button, gated by EstopOK")
    with Rung(
        EstopOK,
        Or(
            And(State == SORTING, IsLarge, Auto),
            And(Manual, DiverterBtn),
        ),
    ):
        out(DiverterCmd)

The diverter rung reads State and IsLarge directly from the state machine in Lesson 7 — no intermediate latch needed. Both control sources fold into one rung with a single out(DiverterCmd). Remember "order has meaning" from Lesson 1? This is how you escape it: one coil, one rung. If two separate rungs both out the same tag, the last one evaluated wins — a false manual rung below a true auto rung would de-energize the diverter. Fold every reason the output should energize into one rung and order stops being a side effect.

Key concept: atomic rungs

All conditions evaluate before any instructions execute. The branch doesn't "see" results of instructions above it in the same rung — every rung is a snapshot of the world, evaluated then acted on as a unit. This is the atomic rung property: conditions read from the state as it was when the rung started, not from half-finished instruction results. It ties back to Lesson 1's scan cycle and forward to Testing, where deterministic scans make this guarantee testable.

Seal-in: a branch that holds itself

Lesson 3 used latch/reset for start/stop control. The classic ladder alternative is a seal-in — a single rung where the output feeds back into its own branch:

with Rung(~StopBtn):
    with branch(Or(StartBtn, Running)):
        out(Running)

Running appears in its own branch condition. Press StartBtn and Running energizes; release it and Running still powers the branch — it holds itself in. Open ~StopBtn and the parent rung drops, breaking the seal. Reach for latch/reset when clarity matters; expect seal-in in every legacy ladder you inherit.

Try it

from pyrung import PLC

with PLC(logic) as plc:
    StopBtn.value = True             # NC inputs: True = healthy
    EstopOK.value = True

    Auto.value = True
    StartBtn.value = True
    plc.step()
    assert Running.value is True
    assert ConveyorMotor.value is True
    assert StatusLight.value is True

    StartBtn.value = False
    plc.step()
    assert Running.value is True     # Still running (latched)

    # E-stop kills everything (NC opens)
    EstopOK.value = False
    plc.step()
    assert ConveyorMotor.value is False
    assert StatusLight.value is False
    assert Running.value is False

Exercise

Add a ManualLight that is on only in manual mode. Write a test that switches from Auto to Manual mid-run and verifies the diverter control source changes without the motor stopping. Test that the diverter button does nothing in auto mode.


Each bin has a count, the diverter has a state, and there's a mode selector. That's a lot of scattered tags. In a real PLC, you'd group them into structures -- UDTs for the bins and the equipment.

Also known as...

OR is a parallel branch of contacts; AND is contacts in series. branch() is "parallel branch" everywhere. The safety-gate pattern is sometimes called "Master Control Reset" (MCR). Seal-in is the classic OR-branch with a series stop contact.