Pythonic ISPL


Using py-mcmas, you can write ISPL specifications directly in python in a straightforward way. One reason this might be convenient is that ISPL specifications are more like data than like programs anyway. But there are other reasons too. For example:

  • InitState sections can be tedious to setup, and it's nice to leverage pythonic iteration or use math.comb for generating combinations.
  • Vars sections can frequently benefit from pythonic dictionary-comprehensions.
  • Agents and Actions sections can use a kind of inheritance.

Using sympy-style symbols and operators instead of logical formulae strings is frequently harder to read, but is often easier to generate, and is often more composable. Pythonic-ISPL has additional capabilities too, such as support for fragments, so that specifications can be built up gradually. Specifications can also be sliced apart, and have a built-in algebra.

Let's start with a basic example.

Minimal Example


Let's make a first pass at expressing the minimal ISPL example in pure python. This tries to avoid "magic strings" of raw ISPL and raw logical expressions, and instead makes use of symbols and sympy expressions1.

You can run it with the ispl command line tool like this:

# Converts python spec -> ISPL source
ispl --python tests/data/minimal.py

# Run a simulation for the spec
ispl --python tests/data/minimal.py --sim

As a 1:1 translation from the ISPL, the "minimal" example below doesn't feel that small! We'll get to more abbreviated version next.

Summary
"""
A 1:1 port of `minimal.ispl` to python.
Run this using `ispl --sim tests/data/minimal.py`
See the docs https://mattvonrocketstein.github.io/py-mcmas/demos/pythonic-ispl
"""

# Imports are implied with `ispl` CLI, but add them anyway
from mcmas import ISPL, Agent, And, Environment, Eq, If, symbols

__spec__ = ISPL(
    title="Minimal valid ISPL definition in Python",
    environment=Environment(
        vars=dict(p=symbols.boolean, q=symbols.boolean),
        actions=[symbols.tick],
        protocol=dict(Other=[symbols.tick]),
    ),
    agents={
        "Player1": Agent(
            vars={"ticking": symbols.boolean},
            actions=[symbols.tick],
            protocol=["Other : {tick}"],
            evolution=[
                If(
                    Eq(symbols.ticking, symbols.true),
                    Eq(symbols.Action, symbols.tick),
                )
            ],
        )
    },
    evaluation=[
        # Equivalently: p if Environment.p=true; q if Environment.q=true
        If(symbols.p, Eq(symbols.Environment.p, symbols.true)),
        If(symbols.q, Eq(symbols.Environment.q, symbols.true)),
    ],
    init_states=[
        # Equivalently: Environment.p=true and Environment.q=false
        And(
            Eq(symbols.Environment.p, symbols.true),
            Eq(symbols.Environment.q, symbols.false),
        )
    ],
    formulae=["p; !q; p -> !q;"],
)

Keeping in mind the goal for this example.. we really only needed the "no-op" agent so that the ISPL would validate and execute. Let's introduce some notation so that defaults are safe to omit and make a few other improvements.

Summary
"""
A more idiomatic/abbreviated version of `minimal.py`
Run this using `ispl --sim tests/data/minimal2.py`
See the docs https://mattvonrocketstein.github.io/py-mcmas/demos/pythonic-ispl
"""

from mcmas import ISPL, And, Environment, false, symbols, true
from mcmas.logic import types

__spec__ = ISPL(...)(
    title="Minimal valid ISPL definition in Python",
    environment=Environment(...)(vars={var: types.bool for var in "pq"}),
    evaluation=[
        symbols.p >> symbols.Environment.p @ true,
        symbols.q >> symbols.Environment.q @ true,
    ],
    init_states=[
        And(
            symbols.Environment.p @ true,
            symbols.Environment.q @ false,
        )
    ],
    formulae=["p", "!q", "p -> !q"],
)

This version has several differences:

  • Statements such as ISPL(...)(**kwargs) return a minimum legal construct + overrides. This is called ellipsis notation and is described in more detail here.
  • We drop the If(..) statements in favor of >> for logical expressions
  • We use a dictionary-comprehension to define vars section.

Flexibile Parsing


At a high level, you'll want to use the pydantic models for api://mcmas.Environment and api://mcmas.Agent, which mirror their counterpart primitives in pure-ISPL.

For individual sections of ISPL, the JSON schema for output is strict, but input-parsing is flexible, and most things that seem to make sense will just work. For example, an Actions block in pure ISPL might look like this:

Agent foo 
...
Actions = {keep,swap,none};
...
end Agent


Translated to python, this could look like any of the following:

from mcmas.ispl import Agent, symbols 
Agent(..., actions='{keep, swap, none};',...)
Agent(..., actions=['keep', 'swap', 'none'],...)
Agent(..., actions=[symbols.keep, symbols.swap,symbols.none],...)


Most sections accept strings, sympy symbols or expressions that can be converted to strings, lists of strings and symbols. The situation is similar with other sections like Vars, ObsVars, and Lobsvars, except that dictionaries are also supported.

Logical Expressions


We've already met symbols previously, which is basically just a factory for sympy symbols1. Get a symbol by simply mentioning it, and combine them in natural ways like this:

from mcmas.logic import symbols 
assert symbols.x + symbols.y == "x+y"


Symbols are actually callable, so that e.g. symbol.K(symbol.predicate) is one way to use the knowledge operator. For other kinds of expressions, you can use things from mcmas.logic like this:

from mcmas import logic
logic.If(symbols.p, symbols.q) # -> p if q
logic.Eq(symbols.x, symbols.y) # -> x=y
logic.And(symbols.x, symbols.y) # -> x and y


Compared to a simple string, this is frequently harder to read! But it's more composable, and now we're closer to a real abstract syntax tree, perhaps opening up future possibilities for leveraging different combinations of LLMS + tree-sitter2, and genetic programming 3. Preference for operators instead of functions can be a decent and more abbreviated compromise here, and we've already met >> as shorthand for If. See also the spec-algebra demos for more details along those lines.

References