From be2f685d651064fd5e6aa2ba2f8585c2137c13cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nurzhan=20Sak=C3=A9n?= Date: Thu, 4 Jan 2024 01:44:14 +0400 Subject: [PATCH] Add simple Bevy integration --- Cargo.toml | 4 + examples/simple.rs | 75 +++++++++++++ petnat_derive/Cargo.toml | 5 +- petnat_derive/src/lib.rs | 36 ------ src/lib.rs | 12 +- src/net.rs | 230 +++++++++++++++++++++------------------ src/net/place.rs | 12 +- src/net/trans.rs | 48 +++----- src/plugin.rs | 43 ++++++-- src/token.rs | 15 ++- 10 files changed, 275 insertions(+), 205 deletions(-) create mode 100644 examples/simple.rs diff --git a/Cargo.toml b/Cargo.toml index d20f4d7..943ed65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,10 @@ categories = ["simulation", "game-development", "science"] [dependencies] bevy_ecs = { version = "0.12", default-features = false } bevy_app = { version = "0.12" } +bevy_utils = { version = "0.12" } petnat_derive = { path = "petnat_derive" } +[dev-dependencies] +bevy = { version = "0.12" } + [features] \ No newline at end of file diff --git a/examples/simple.rs b/examples/simple.rs new file mode 100644 index 0000000..7799c68 --- /dev/null +++ b/examples/simple.rs @@ -0,0 +1,75 @@ +//! A simple Petri net being manipulated via systems. + +use bevy::input::common_conditions::input_just_pressed; +use bevy::prelude::*; +use petnat::{NetId, Nn, PetriNet, PetriNetPlugin, Place, Pn, Tn, Token, W}; +use std::any::type_name; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(PetriNetPlugin::> { + build: |builder| { + builder + .add_place::>() + .add_place::>() + .add_place::>() + // T0 requires 1 token in P0 and 2 tokens in P1 to be enabled + // and it will produce 1 token in P2 when fired + .add_trans::, ((Pn<0>, W<1>), (Pn<1>, W<2>)), (Pn<2>, W<1>)>() + }, + }) + .add_systems(Startup, spawn_token::>) + .add_systems( + Update, + ( + // press 1 and 2 to mark `P0` and `P1` + mark::, Pn<0>>.run_if(input_just_pressed(KeyCode::Key1)), + mark::, Pn<1>>.run_if(input_just_pressed(KeyCode::Key2)), + // press T to fire `T0` + trans_t0::>.run_if(input_just_pressed(KeyCode::T)), + print_net::>, + ), + ) + .run(); +} + +fn spawn_token(mut commands: Commands, net: Res>) { + commands.spawn(net.spawn_token()); + info!("Spawning a token..."); +} + +fn mark>(net: Res>, mut tokens: Query<&mut Token>) { + for mut token in &mut tokens { + net.mark::

(&mut token, 1); + let (_, name) = type_name::

() + .rsplit_once(':') + .unwrap_or(("", type_name::

())); + info!("{} marked!", name); + } +} + +fn trans_t0(net: Res>, mut tokens: Query<&mut Token>) { + for mut token in &mut tokens { + if let Some(()) = net.fire::>(token.bypass_change_detection()) { + info!("T0 fired!"); + token.set_changed(); + } else { + info!("T0 cannot fire! (Need: 1 in P0 + 2 in P1)"); + } + } +} + +fn print_net( + net: Res>, + tokens: Query<(Entity, &Token), Changed>>, +) { + for (id, token) in &tokens { + info!("== TOKEN {:?} STATE ==", id); + info!("P0: {}", token.marks::>()); + info!("P1: {}", token.marks::>()); + info!("T0 enabled: {}", net.enabled::>(token)); + info!("P2: {}", token.marks::>()); + info!("====================="); + } +} diff --git a/petnat_derive/Cargo.toml b/petnat_derive/Cargo.toml index b214b9a..49bc79f 100644 --- a/petnat_derive/Cargo.toml +++ b/petnat_derive/Cargo.toml @@ -1,12 +1,9 @@ [package] name = "petnat_derive" -version = "0.1.0" +version = "0.0.1" edition = "2021" [lib] proc-macro = true [dependencies] -syn = { version = "2.0.44" } -quote = { version = "1.0.34" } -deluxe = { version = "0.5.0" } \ No newline at end of file diff --git a/petnat_derive/src/lib.rs b/petnat_derive/src/lib.rs index 41084d9..b8d7be1 100644 --- a/petnat_derive/src/lib.rs +++ b/petnat_derive/src/lib.rs @@ -1,37 +1 @@ extern crate proc_macro; - -use proc_macro::TokenStream; -use quote::quote; -use syn::{parse_macro_input, DeriveInput}; - -#[proc_macro_derive(Place)] -pub fn place_derive(input: TokenStream) -> TokenStream { - // Parse the input tokens into a syntax tree - let input = parse_macro_input!(input as DeriveInput); - - // Extract the name of the struct or enum - let ident = &input.ident; - - let expanded = quote! { - impl Place for #ident {} - }; - - // Convert the generated code into a TokenStream - TokenStream::from(expanded) -} - -#[proc_macro_derive(Trans)] -pub fn trans_derive(input: TokenStream) -> TokenStream { - // Parse the input tokens into a syntax tree - let input = parse_macro_input!(input as DeriveInput); - - // Extract the name of the struct or enum - let ident = &input.ident; - - let expanded = quote! { - impl Trans for #ident {} - }; - - // Convert the generated code into a TokenStream - TokenStream::from(expanded) -} diff --git a/src/lib.rs b/src/lib.rs index bf018f8..86b74cc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,9 +3,15 @@ #![deny(missing_docs)] #![deny(clippy::all)] -pub mod net; -pub mod plugin; -pub mod token; +pub use crate::net::place::{Place, PlaceId, Pn}; +pub use crate::net::trans::{Arcs, Tn, Trans, TransId, W}; +pub use crate::net::{NetId, Nn, PetriNet, PetriNetBuilder}; +pub use crate::plugin::PetriNetPlugin; +pub use crate::token::Token; + +mod net; +mod plugin; +mod token; // todo: // - petri net resource / component diff --git a/src/net.rs b/src/net.rs index 38b2e0d..677eb29 100644 --- a/src/net.rs +++ b/src/net.rs @@ -1,100 +1,41 @@ //! Petri net. -use std::collections::HashMap; use std::fmt::Debug; -use crate::net::place::ErasedPlace; -use place::{Place, PlaceId}; -use trans::{Arcs, ErasedTrans, Trans, TransId}; +use bevy_ecs::system::Resource; +use bevy_utils::StableHashMap; +use crate::net::place::{ErasedPlace, Place, PlaceId}; +use crate::net::trans::{Arcs, ErasedTrans, Trans, TransId}; use crate::token::Token; pub mod place; pub mod trans; -/// Petri net id. +/// Label for [`PetriNet`]. pub trait NetId: Send + Sync + 'static {} +/// Numbered [`NetId`] for convenience. +pub enum Nn {} + +impl NetId for Nn {} + /// Petri net. -/// TODO: remove HashSet and HashMap in favor of an array + a PHF -/// since the net will probably always be constructed at compile time? -/// TODO: make this a Bevy resource -#[derive(Clone, Debug)] +#[derive(Resource, Clone, Debug)] pub struct PetriNet { - places: HashMap, ErasedPlace>, - transitions: HashMap, ErasedTrans>, + places: StableHashMap, ErasedPlace>, + transitions: StableHashMap, ErasedTrans>, } impl PetriNet { - /// Returns an empty net. - pub fn empty() -> Self { - Self { - places: HashMap::new(), - transitions: HashMap::new(), + /// Creates a [`PetriNet`] builder. + pub fn builder() -> PetriNetBuilder { + PetriNetBuilder { + places: StableHashMap::default(), + transitions: StableHashMap::default(), } } - /// Adds a place. - pub fn add_place>(mut self) -> Self { - self.add_place_by_id(P::erased()); - self - } - - /// Adds a transition and its input and output arcs. - pub fn add_trans, I: Arcs, O: Arcs>(mut self) -> Self { - self.add_trans_by_id(T::erased(), I::erased(), O::erased()); - self - } - - fn add_place_by_id(&mut self, place: PlaceId) { - assert!( - !self.places.contains_key(&place), - "Attempted to add place {:?} twice.", - place - ); - self.places.insert( - place, - ErasedPlace { - producers: vec![], - consumers: vec![], - }, - ); - } - - fn add_trans_by_id( - &mut self, - trans: TransId, - preset: Vec<(PlaceId, usize)>, - postset: Vec<(PlaceId, usize)>, - ) { - assert!( - !self.transitions.contains_key(&trans), - "Attempted to add transition {:?} twice.", - trans - ); - for (place, weight) in preset.iter() { - let Some(place) = self.places.get_mut(place) else { - panic!("Input place {place:?} not found."); - }; - place.consumers.push((trans, *weight)); - } - for (place, weight) in postset.iter() { - let Some(place) = self.places.get_mut(place) else { - panic!("Output place {place:?} not found."); - }; - place.producers.push((trans, *weight)); - } - self.transitions.insert( - trans, - ErasedTrans { - join: preset, - split: postset, - }, - ); - } -} - -impl PetriNet { /// Spawns new token. pub fn spawn_token(&self) -> Token { Token::new() @@ -105,12 +46,12 @@ impl PetriNet { self.get_enabled_by_id(T::erased(), token).is_some() } - /// Fires transition. + /// Fires a transition. pub fn fire>(&self, token: &mut Token) -> Option<()> { self.fire_by_id(T::erased(), token) } - /// Fires all enabled transitions once. + /// Fires all transitions once. pub fn fire_all(&self, token: &mut Token) -> Vec> { self.transitions .keys() @@ -119,10 +60,17 @@ impl PetriNet { .collect() } - /// Fires enabled transitions until none are left. + /// Fires transitions until the net is dead. + /// + /// Will loop forever if the token moves in a cycle. pub fn fire_while(&self, token: &mut Token) { loop { - let Some(trans) = self.iter_enabled(token).next() else { + let Some(trans) = self + .transitions + .keys() + .copied() + .find(|trans| self.get_enabled_by_id(*trans, token).is_some()) + else { break; }; self.fire_by_id(trans, token); @@ -159,17 +107,6 @@ impl PetriNet { .then_some(trans) } - /// Returns an iterator over the enabled transitions for a token. FIXME: extract into struct - fn iter_enabled<'a>( - &'a self, - token: &'a Token, - ) -> impl Iterator> + 'a { - self.transitions - .keys() - .copied() - .filter(|trans| self.get_enabled_by_id(*trans, token).is_some()) - } - /// Fires transition. fn fire_by_id(&self, trans: TransId, token: &mut Token) -> Option<()> { let ErasedTrans { join, split } = self.get_enabled_by_id(trans, token)?; @@ -181,10 +118,86 @@ impl PetriNet { } } +/// [`PetriNet`] builder. +pub struct PetriNetBuilder { + places: StableHashMap, ErasedPlace>, + transitions: StableHashMap, ErasedTrans>, +} + +impl PetriNetBuilder { + /// Adds a [`Place`] to the net. + pub fn add_place>(mut self) -> Self { + self.add_place_by_id(P::erased()); + self + } + + /// Adds a [`Trans`] and its input and output [`Arcs`] to the net. + pub fn add_trans, I: Arcs, O: Arcs>(mut self) -> Self { + self.add_trans_by_id(T::erased(), I::erased(), O::erased()); + self + } + + /// Returns the constructed [`PetriNet`]. + pub fn build(self) -> PetriNet { + // todo: validation + PetriNet { + places: self.places, + transitions: self.transitions, + } + } + + fn add_place_by_id(&mut self, place: PlaceId) { + assert!( + !self.places.contains_key(&place), + "Attempted to add place {:?} twice.", + place + ); + self.places.insert( + place, + ErasedPlace { + producers: vec![], + consumers: vec![], + }, + ); + } + + fn add_trans_by_id( + &mut self, + trans: TransId, + preset: Vec<(PlaceId, usize)>, + postset: Vec<(PlaceId, usize)>, + ) { + assert!( + !self.transitions.contains_key(&trans), + "Attempted to add transition {:?} twice.", + trans + ); + for (place, weight) in preset.iter() { + let Some(place) = self.places.get_mut(place) else { + panic!("Input place {place:?} not found."); + }; + place.consumers.push((trans, *weight)); + } + for (place, weight) in postset.iter() { + let Some(place) = self.places.get_mut(place) else { + panic!("Output place {place:?} not found."); + }; + place.producers.push((trans, *weight)); + } + self.transitions.insert( + trans, + ErasedTrans { + join: preset, + split: postset, + }, + ); + } +} + #[cfg(test)] mod tests { use super::place::Place; - use super::trans::{Trans, W1, W2, W3}; + use super::trans::{Trans, W}; use super::{NetId, PetriNet}; enum Minimal {} @@ -219,44 +232,48 @@ mod tests { // (p0) -> |t0| -> (p1) fn minimal() -> PetriNet { - PetriNet::empty() + PetriNet::builder() .add_place::() .add_place::() - .add_trans::() + .add_trans::), (P1, W<1>)>() + .build() } // Transitions with no input places are token sources, // and transitions with no output places are token sinks // |t0| -> (p0) -> |t1| fn producer_consumer() -> PetriNet { - PetriNet::empty() + PetriNet::builder() .add_place::() - .add_trans::() - .add_trans::() + .add_trans::)>() + .add_trans::), ()>() + .build() } // (p0) -\ /-> (p2) // >-> |t0| --<--> (p3) // (p1) -/ \-> (p4) fn weighted_star() -> PetriNet { - PetriNet::empty() + PetriNet::builder() .add_place::() .add_place::() .add_place::() .add_place::() .add_place::() - .add_trans::() + .add_trans::), (P1, W<2>)), ((P2, W<1>), (P3, W<2>), (P4, W<3>))>() + .build() } // Two places sending a token back and forth through two transitions in opposite directions: // /--> |t0| -> (p1) // (p0) <- |t1| <--/ fn ring() -> PetriNet { - PetriNet::empty() + PetriNet::builder() .add_place::() .add_place::() - .add_trans::() - .add_trans::() + .add_trans::), (P1, W<1>)>() + .add_trans::), (P0, W<1>)>() + .build() } // Two transitions sharing a preset place. When one of them fires, the other ceases to be enabled. @@ -264,13 +281,14 @@ mod tests { // (p1) -< >-> (p3) // (p2) --> |t1| -/ fn choice() -> PetriNet { - PetriNet::empty() + PetriNet::builder() .add_place::() .add_place::() .add_place::() .add_place::() - .add_trans::() - .add_trans::() + .add_trans::), (P1, W<1>)), (P3, W<1>)>() + .add_trans::), (P2, W<1>)), (P3, W<1>)>() + .build() } #[test] diff --git a/src/net/place.rs b/src/net/place.rs index c60271f..44f3c8a 100644 --- a/src/net/place.rs +++ b/src/net/place.rs @@ -1,12 +1,13 @@ //! Petri net places. -use crate::net::trans::TransId; -use crate::net::NetId; use std::any::TypeId; use std::fmt::{Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; +use crate::net::trans::TransId; +use crate::net::NetId; + /// Reference to a place in a net. pub struct PlaceId(TypeId, PhantomData); @@ -45,8 +46,6 @@ impl Debug for PlaceId { /// /// May represent different concepts depending on the context, /// commonly used to represent some state or condition. -/// -/// TODO: derive macro pub trait Place where Self: Send + Sync + 'static, @@ -57,6 +56,11 @@ where } } +/// Numbered [`Place`] compatible with any [`PetriNet`] for convenience. +pub enum Pn {} + +impl Place for Pn {} + #[derive(Clone, Debug)] pub(crate) struct ErasedPlace { pub producers: Vec<(TransId, usize)>, diff --git a/src/net/trans.rs b/src/net/trans.rs index 8978760..c53c5cd 100644 --- a/src/net/trans.rs +++ b/src/net/trans.rs @@ -1,12 +1,13 @@ //! Petri net transitions. -use crate::net::place::{Place, PlaceId}; -use crate::net::NetId; use std::any::TypeId; use std::fmt::{Debug, Formatter}; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; +use crate::net::place::{Place, PlaceId}; +use crate::net::NetId; + /// Reference to a transition in a net. pub struct TransId(TypeId, PhantomData); @@ -42,8 +43,6 @@ impl Debug for TransId { } /// Transition in a Petri net. -/// -/// TODO: derive macro pub trait Trans where Self: Send + Sync + 'static, @@ -54,6 +53,11 @@ where } } +/// Numbered [`Trans`] compatible with any [`PetriNet`] for convenience. +pub enum Tn {} + +impl Trans for Tn {} + #[derive(Clone, Debug)] pub(crate) struct ErasedTrans { pub join: Vec<(PlaceId, usize)>, @@ -61,30 +65,7 @@ pub(crate) struct ErasedTrans { } /// Arc weight. -pub trait Weight { - /// Arc weight value. - const W: usize; -} - -/// Weight of 1. -pub struct W1; - -/// Weight of `W` + 1. -pub struct Succ(PhantomData); - -impl Weight for W1 { - const W: usize = 1; -} - -impl Weight for Succ { - const W: usize = W::W + 1; -} - -/// Weight of 2. -pub type W2 = Succ; - -/// Weight of 3. -pub type W3 = Succ; +pub enum W {} /// Weighted place-transition arcs. pub trait Arcs { @@ -95,13 +76,13 @@ pub trait Arcs { macro_rules! impl_arcs { ($(($place:ident, $weight:ident)),*) => { #[allow(unused_parens)] - impl Arcs for ($(($place, $weight)),*) + impl Arcs for ($(($place, W<$weight>)),*) where Net: NetId, - $($place: Place, $weight: Weight),* + $($place: Place),* { fn erased() -> Vec<(PlaceId, usize)> { - vec![$(($place::erased(), $weight::W)),*] + vec![$(($place::erased(), $weight)),*] } } }; @@ -111,14 +92,13 @@ impl_arcs!(); impl_arcs!((P0, W0)); // 1-tuple case -impl Arcs for ((P0, W0),) +impl Arcs for ((P0, W),) where Net: NetId, P0: Place, - W0: Weight, { fn erased() -> Vec<(PlaceId, usize)> { - vec![(P0::erased(), W0::W)] + vec![(P0::erased(), W0)] } } diff --git a/src/plugin.rs b/src/plugin.rs index c1b026f..4e64a88 100644 --- a/src/plugin.rs +++ b/src/plugin.rs @@ -1,28 +1,47 @@ //! Bevy plugin. +use std::marker::PhantomData; + +use bevy_app::{App, Plugin}; +use bevy_ecs::change_detection::DetectChangesMut; +use bevy_ecs::system::{Query, Res}; + use crate::net::place::Place; use crate::net::trans::Trans; use crate::net::{NetId, PetriNet}; use crate::token::Token; -use std::marker::PhantomData; +use crate::PetriNetBuilder; -/// TODO: flesh out plugin api +/// Plugin that initializes and manages a [`PetriNet`]. pub struct PetriNetPlugin { - _net: PhantomData, + /// Function used to build the [`PetriNet`]. + /// FIXME: feels clunky? + pub build: fn(PetriNetBuilder) -> PetriNetBuilder, +} + +impl Plugin for PetriNetPlugin { + fn build(&self, app: &mut App) { + let builder = PetriNet::builder(); + let pnet = (self.build)(builder).build(); + app.insert_resource(pnet); + } } /// TODO: how to resolve choices? -fn _transition_system(net: PetriNet, mut tokens: Vec<&mut Token>) { - for token in &mut tokens { - net.fire_all(token); +fn _transition_system(net: Res>, mut tokens: Query<&mut Token>) { + for mut token in &mut tokens { + let fired = net.fire_all(token.bypass_change_detection()); + if !fired.is_empty() { + token.set_changed() + } } } /// TODO: Possible event for queueing an attempt to fire a Petri net. -pub struct TransQueueEvent>(PhantomData<(Net, T)>); +pub struct _TransQueueEvent>(PhantomData<(Net, T)>); fn _example_immediate_event>( - queue_events: Vec>, + queue_events: Vec<_TransQueueEvent>, net: PetriNet, mut tokens: Vec<&mut Token>, ) { @@ -34,14 +53,14 @@ fn _example_immediate_event>( } /// TODO: Possible event for queueing a marking of a Petri net place. -pub struct PlaceMarkEvent>(usize, PhantomData<(Net, P)>); +pub struct _PlaceMarkEvent>(usize, PhantomData<(Net, P)>); fn _example_mark_place_with_event>( - mark_events: Vec>, + mark_events: Vec<_PlaceMarkEvent>, net: PetriNet, mut tokens: Vec<&mut Token>, ) { - for PlaceMarkEvent(n, _) in mark_events.iter() { + for _PlaceMarkEvent(n, _) in mark_events.iter() { for token in &mut tokens { net.mark::

(token, *n); } @@ -50,6 +69,6 @@ fn _example_mark_place_with_event>( /// TODO: Possible event signifying that a Petri net transition has been fired. /// unclear how to do this properly, do we generate an event per transition always, no matter who fired it? -pub struct TransEvent>(PhantomData<(Net, T)>); +pub struct _TransEvent>(PhantomData<(Net, T)>); // TODO: place marking sends event - same as the previous issue diff --git a/src/token.rs b/src/token.rs index 095982e..7c40226 100644 --- a/src/token.rs +++ b/src/token.rs @@ -4,12 +4,13 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::marker::PhantomData; +use bevy_ecs::component::Component; + use crate::net::place::{Place, PlaceId}; use crate::net::NetId; /// Petri net token. Holds the state of the net execution. -/// TODO: make this a Bevy component -#[derive(Clone, Eq, PartialEq, Debug)] +#[derive(Component, Clone, Eq, PartialEq, Debug)] pub struct Token { marking: HashMap, usize>, _net: PhantomData, @@ -65,10 +66,11 @@ impl Token { #[cfg(test)] mod tests { - use super::*; - use crate::net::trans::{Trans, W1}; + use crate::net::trans::{Trans, W}; use crate::net::PetriNet; + use super::*; + enum N0 {} enum P0 {} enum T0 {} @@ -80,9 +82,10 @@ mod tests { const N: usize = 3; fn net() -> PetriNet { - PetriNet::empty() + PetriNet::builder() .add_place::() - .add_trans::() + .add_trans::), ()>() + .build() } #[test]