pflow-rs
Rust port of go-pflow — Petri net modeling with ODE simulation and token model DSL.
Crates
| Crate | Description |
|---|---|
pflow-core |
Petri net types (Place, Transition, Arc), fluent Builder API, state map utilities |
pflow-solver |
ODE solvers (Tsit5, RK45, RK4, Euler, Heun, Midpoint, BS32), implicit methods, equilibrium detection |
pflow-tokenmodel |
Token model schema, snapshot, runtime execution, validation, content-addressed identity |
pflow-dsl |
S-expression DSL: lexer, parser, interpreter, builder, codegen |
pflow-macros |
schema! proc macro — compile-time DSL parsing with zero runtime overhead |
pflow-zk |
ZK proof traits (PetriProver), IncidenceMatrix extraction, fire_transition() |
pflow-zk-arkworks |
Groth16 prover over BN254 with Poseidon hashing (structural R1CS) |
pflow-zk-risc0 |
risc0 zkVM STARK prover (simulation mode default; real proofs with prove feature) |
pflow |
Umbrella crate re-exporting all of the above |
Quick Start
use *;
// Build an SIR epidemic model
let = build
.sir
.with_rates;
// Solve to equilibrium
let state = net.set_state;
let prob = new;
let = find_equilibrium;
assert!;
// S + I + R = 1000 (conserved)
Petri Net Builder
use PetriNet;
let net = build
.place
.place
.transition
.arc
.arc
.done;
Chain helper for linear sequences:
let net = build
.chain
.done;
ODE Solver
Seven explicit Runge-Kutta methods plus implicit solvers for stiff systems:
use *;
let prob = new;
// Explicit (adaptive step size)
let sol = solve;
// Implicit (stiff systems)
let sol = implicit_euler;
let sol = trbdf2;
// Auto-detect stiffness
let sol = solve_implicit;
Solver presets:
| Preset | Use Case |
|---|---|
Options::default_opts() |
General purpose |
Options::fast() |
Game AI, interactive (~10x faster) |
Options::accurate() |
Research, publishing |
Options::game_ai() |
Move evaluation |
Options::epidemic() |
SIR/SEIR models |
Token Model DSL
Define token model schemas using an S-expression DSL. The schema! macro parses and validates the DSL at compile time — syntax errors become compiler errors, and the generated code constructs the Schema directly with zero runtime parsing.
use schema;
let s = schema!;
assert_eq!;
assert_eq!;
Invalid DSL is caught at compile time:
// This won't compile:
let s = schema!;
// error: DSL parse error: expected symbol "schema", got "bad"
DSL Syntax Reference
(schema <name>
(version <version>)
(states
(state <id> :kind token :initial <n>) ; token state with initial count
(state <id> :type <type> :exported) ; data state, exported
)
(actions
(action <id>) ; simple action
(action <id> :guard {<expr>}) ; guarded action
)
(arcs
(arc <source> -> <target>) ; simple arc
(arc <source> -> <target> :keys (<k1> <k2>)) ; arc with map keys
(arc <source> -> <target> :value <binding>) ; arc with value binding
)
(constraints
(constraint <id> {<expr>}) ; invariant constraint
)
)
Alternative: Fluent Builder
For dynamic schema construction, use the builder API directly:
use Builder;
let schema = new
.data.exported
.data
.action.guard
.flow.keys
.flow.keys
.constraint
.must_schema;
Runtime Parsing
For DSL strings loaded at runtime (e.g. from files):
use parse_schema;
let schema = parse_schema.unwrap;
Content-Addressed Identity
Schemas produce deterministic content identifiers (CIDs) via SHA-256. Insertion order doesn't matter — schemas with the same structure always produce the same hash.
use schema;
let s = schema!;
// Full CID (includes name/version)
let cid = s.cid; // "cid:a1b2c3..."
// Structural fingerprint (ignores name/version)
let idh = s.identity_hash; // "idh:d4e5f6..."
// Compare schemas
assert!; // same CID
assert!; // same structure
Runtime Execution
use ;
use Value;
let mut rt = new;
// Set initial balance
rt.snapshot.set_data_map_value;
// Execute a transfer
let mut bindings = new;
bindings.insert;
bindings.insert;
bindings.insert;
rt.execute_with_bindings.unwrap;
// alice: 750, bob: 250
State Utilities
use stateutil;
let state = /* ... */;
let updated = apply;
let total = sum;
let changes = diff;
let history = filter;
Code Generation
Generate Rust source from DSL definitions:
use generate_rust_from_dsl;
let code = generate_rust_from_dsl.unwrap;
// Outputs a Rust function that constructs the schema
ZK Proofs
Two contrasting strategies prove the same statement — "transition T is enabled and transforms marking M into M'":
- arkworks (structural R1CS): compiles net topology into Groth16 constraints over BN254 with Poseidon hashing. Constant 128-byte proofs, sub-millisecond verification.
- risc0 (zkVM STARK): wraps transition logic in a RISC-V guest program. No trusted setup, but larger proofs and slower proving.
use ;
use ArkworksProver;
let net = build.sir.done;
let matrix = from_petri_net;
let mut prover = new;
prover.setup.unwrap;
let pre = matrix.initial_marking;
let post = fire_transition.unwrap;
let witness = TransitionWitness ;
let proof = prover.prove.unwrap;
assert!;
assert_eq!;
Benchmarks
Head-to-head comparison using NxN tic-tac-toe nets (3n² places, 2n² transitions):
cargo run --example zk_compare -p pflow --features zk-arkworks,zk-risc0-prove --release
| Model | System | Places | Trans | Setup | Prove | Verify | Proof Size |
|---|---|---|---|---|---|---|---|
| SIR | groth16-bn254 | 3 | 2 | 26ms | 18ms | 1.0ms | 128B |
| SIR | risc0 | 3 | 2 | 0ms | 3.0s | 9.5ms | 217KB |
| TTT 3x3 | groth16-bn254 | 27 | 18 | 67ms | 78ms | 0.8ms | 128B |
| TTT 3x3 | risc0 | 27 | 18 | 0ms | 6.1s | 10ms | 239KB |
| TTT 5x5 | groth16-bn254 | 75 | 50 | 176ms | 189ms | 0.8ms | 128B |
| TTT 5x5 | risc0 | 75 | 50 | 0ms | 12.1s | 11ms | 250KB |
| TTT 10x10 | groth16-bn254 | 300 | 200 | 654ms | 709ms | 0.9ms | 128B |
| TTT 10x10 | risc0 | 300 | 200 | 0ms | 51.8s | 13ms | 275KB |
| TTT 20x20 | groth16-bn254 | 1200 | 800 | 2.6s | 2.8s | 0.9ms | 128B |
| TTT 20x20 | risc0 | 1200 | 800 | 0ms | 2m47s | 48ms | 1.05MB |
| TTT 40x40 | groth16-bn254 | 4800 | 3200 | 11.1s | 11.4s | 0.9ms | 128B |
| TTT 40x40 | risc0 | 4800 | 3200 | 0ms | 10m55s | 160ms | 3.5MB |
At 4800 places / 3200 transitions: arkworks proves 57x faster, verifies 178x faster, with proofs 28,000x smaller. Groth16 verification and proof size remain constant regardless of net size. risc0 trades performance for no trusted setup and simpler guest programming.
Feature Flags
= { = ["zk-arkworks"] } # Groth16 prover
= { = ["zk-risc0"] } # risc0 simulation mode
= { = ["zk-risc0-prove"] } # risc0 real STARK proofs (requires cargo risczero install)
Dependencies
Minimal external dependencies:
serde,serde_json— serializationsha2,hex— content-addressed identitythiserror— error types
License
MIT