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:
InitStatesections can be tedious to setup, and it's nice to leverage pythonic iteration or usemath.combfor generating combinations.Varssections can frequently benefit from pythonic dictionary-comprehensions.AgentsandActionssections 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.
"""
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.
"""
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
varssection.
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.