Repository: matthewryanscott/CarlAdam

Issue:
Add examples for all figures in book related to vending machine example
### Current

The [vending_machine example](https://github.com/matthewryanscott/CarlAdam/blob/main/examples/understanding_petri_nets/vending_machine.py) represents Figure 1.10 in the book [Understanding Petri Nets: Modeling Techniques, Analysis Methods, Case Studies](https://link.springer.com/book/10.1007/978-3-642-33278-4).

### Desired

All other figures preceding Figure 1.10 are translated into CarlAdam examples.

### Out of scope

Any figures following Figure 1.10 can be tracked in other tickets.

Discussion:

Patch:
diff --git a/carladam/petrinet/color.py b/carladam/petrinet/color.py
index 85eb87d..786e1a3 100644
--- a/carladam/petrinet/color.py
+++ b/carladam/petrinet/color.py
@@ -43,9 +43,9 @@ class Color:
         """Return a generator that produces tokens of this color with the given `data`."""
         from carladam.petrinet.token import Token
 
-        data = pmap(data)
+        pdata = pmap(data)
         while True:
-            yield Token(color=self, data=data)
+            yield Token(color=self, data=pdata)
 
     def produce(self, quantity: int = 1, **kwargs) -> TransitionFunction:
         """Return a transition function that generates a given quantity of this color of token."""
diff --git a/carladam/petrinet/token.py b/carladam/petrinet/token.py
index 7b1ed55..8c6e5ad 100644
--- a/carladam/petrinet/token.py
+++ b/carladam/petrinet/token.py
@@ -35,12 +35,21 @@ class Token:
         """Return a copy of this `Token` but with `data` replaced with `kwargs`."""
         return Token(id=self.id, name=self.name, color=self.color, data=kwargs)
 
+    def clone(self) -> Token:
+        """Return a clone of this `Token` but having a new `id` and `name`."""
+        return Token(id=default_id(), color=self.color, data=self.data)
+
     def __hash__(self):
         return hash(self.id)
 
     def __lt__(self, other):
         return (self.color.label, self.name) < (other.color.label, other.name)
 
+    def __mul__(self, quantity: int) -> TokenSet:
+        if not isinstance(quantity, int):
+            raise TypeError("Can only multiply tokens by integers")
+        return frozenset(self.clone() for _ in range(quantity))
+
     def __repr__(self):
         if self.name == self.id:
             s = repr(self.color)
diff --git a/examples/understanding_petri_nets/figure_1_01_02.py b/examples/understanding_petri_nets/figure_1_01_02.py
new file mode 100644
index 0000000..583098d
--- /dev/null
+++ b/examples/understanding_petri_nets/figure_1_01_02.py
@@ -0,0 +1,30 @@
+from carladam import Color, PetriNet, Place, Transition
+
+Coin = Color("🪙")
+Packet = Color("📦")
+
+
+class UnderstandingPetriNetsFigure_1_01_02(PetriNet):  # noqa
+    """Understanding Petri Nets: Figures 1.1, 1.2"""
+
+    class Structure:
+        class P:
+            coin_slot = Place()
+            compartment = Place()
+
+        class T:
+            t = Transition(fn=[Packet.produce()])
+
+        arcs = {
+            P.coin_slot >> Coin >> T.t,
+            T.t >> Packet >> P.compartment,
+        }
+
+        example_markings = {
+            "Figure 1.1: The cookie vending machine in its initial state": {
+                P.coin_slot: {Coin()},
+            },
+            "Figure 1.2: The cookie vending machine after the occurrence of t": {
+                P.compartment: {Packet()},
+            },
+        }
diff --git a/examples/understanding_petri_nets/figure_1_03_04_05.py b/examples/understanding_petri_nets/figure_1_03_04_05.py
new file mode 100644
index 0000000..7565e2e
--- /dev/null
+++ b/examples/understanding_petri_nets/figure_1_03_04_05.py
@@ -0,0 +1,64 @@
+from carladam import Abstract, Color, PetriNet, Place, Transition
+
+Coin = Color("🪙")
+Packet = Color("📦")
+Counter = Color("🧮")
+
+
+def x_minus_1(token):
+    return token.replace(x=token.data["x"] - 1)
+
+
+def counter_x_gt_0(inputs):
+    counters = (token for token in inputs if token.color == Counter)
+    return all(counter.data["x"] > 0 for counter in counters)
+
+
+class UnderstandingPetriNetsFigure_1_03_04_05(PetriNet):  # noqa
+    """Understanding Petri Nets: Figures 1.3, 1.4, 1.5"""
+
+    class Structure:
+        class P:
+            cash_box = Place()
+            coin_slot = Place()
+            compartment = Place()
+            signal = Place()
+            storage = Place()
+
+        class T:
+            a = Transition(fn=[Coin.passthrough(), Abstract.produce()])
+            b = Transition(fn=Packet.produce())
+
+        arcs = {
+            P.coin_slot >> Coin >> T.a,
+            T.a >> Coin >> P.cash_box,
+            T.a >> P.signal,
+            P.signal >> T.b,
+            P.storage >> Packet >> T.b,
+            T.b >> Packet >> P.compartment,
+        }
+
+        example_markings = {
+            "Figure 1.3: A look inside the cookie vending machine": {
+                P.coin_slot: {Coin()},
+                P.storage: Packet() * 5,
+            },
+            "Figure 1.4: After the occurrence of a": {
+                P.signal: {Abstract()},
+                P.storage: Packet() * 5,
+            },
+            "Figure 1.5: After the occurrence of b": {
+                P.compartment: {Packet()},
+                P.storage: Packet() * 5,
+            },
+        }
+
+        clusters = {
+            "": {
+                P.coin_slot,
+                T.a,
+                P.signal,
+                T.b,
+                P.compartment,
+            }
+        }
diff --git a/examples/understanding_petri_nets/figure_1_06.py b/examples/understanding_petri_nets/figure_1_06.py
new file mode 100644
index 0000000..e2ecf09
--- /dev/null
+++ b/examples/understanding_petri_nets/figure_1_06.py
@@ -0,0 +1,65 @@
+from carladam import Abstract, Color, PetriNet, Place, Transition
+
+Coin = Color("🪙")
+Packet = Color("📦")
+Counter = Color("🧮")
+
+
+def x_minus_1(token):
+    return token.replace(x=token.data["x"] - 1)
+
+
+def counter_x_gt_0(inputs):
+    counters = (token for token in inputs if token.color == Counter)
+    return all(counter.data["x"] > 0 for counter in counters)
+
+
+class UnderstandingPetriNetsFigure_1_06(PetriNet):  # noqa
+    """
+    Understanding Petri Nets: Figure 1.6
+
+    The cookie vending machine with the cold transitions of its interface
+    """
+
+    class Structure:
+        class P:
+            cash_box = Place()
+            coin_slot = Place()
+            compartment = Place()
+            signal = Place()
+            storage = Place()
+
+        class T:
+            a = Transition(fn=[Coin.passthrough(), Abstract.produce()])
+            b = Transition(fn=Packet.produce())
+            insert_coin = Transition(fn=Coin.produce())
+            take_packet = Transition()
+
+        arcs = {
+            T.insert_coin >> Coin >> P.coin_slot,
+            P.coin_slot >> Coin >> T.a,
+            T.a >> Coin >> P.cash_box,
+            T.a >> P.signal,
+            P.signal >> T.b,
+            P.storage >> Packet >> T.b,
+            T.b >> Packet >> P.compartment,
+            P.compartment >> Packet >> T.take_packet,
+        }
+
+        clusters = {
+            "": {
+                T.insert_coin,
+                P.coin_slot,
+                T.a,
+                P.signal,
+                T.b,
+                P.compartment,
+                T.take_packet,
+            }
+        }
+
+        example_markings = {
+            "Start here": {
+                P.storage: Packet() * 5,
+            },
+        }
diff --git a/examples/understanding_petri_nets/figure_1_07_08_09.py b/examples/understanding_petri_nets/figure_1_07_08_09.py
new file mode 100644
index 0000000..a94620e
--- /dev/null
+++ b/examples/understanding_petri_nets/figure_1_07_08_09.py
@@ -0,0 +1,75 @@
+from carladam import Abstract, Color, PetriNet, Place, Transition
+
+Coin = Color("🪙")
+Packet = Color("📦")
+Counter = Color("🧮")
+
+
+def x_minus_1(token):
+    return token.replace(x=token.data["x"] - 1)
+
+
+def counter_x_gt_0(inputs):
+    counters = (token for token in inputs if token.color == Counter)
+    return all(counter.data["x"] > 0 for counter in counters)
+
+
+class UnderstandingPetriNetsFigure_1_07_08_09(PetriNet):  # noqa
+    """
+    Understanding Petri Nets: Figures 1.7, 1.8, 1.9
+
+    The cookie vending machine with the option of returning coins.
+    """
+
+    class Structure:
+        class P:
+            cash_box = Place()
+            coin_slot = Place()
+            compartment = Place()
+            signal = Place()
+            storage = Place()
+
+        class T:
+            a = Transition(fn=[Coin.passthrough(), Abstract.produce()])
+            b = Transition(fn=Packet.produce())
+            insert_coin = Transition(fn=Coin.produce())
+            take_packet = Transition()
+            return_coin = Transition()
+
+        arcs = {
+            T.insert_coin >> Coin >> P.coin_slot,
+            P.coin_slot >> Coin >> T.return_coin,
+            P.coin_slot >> Coin >> T.a,
+            T.a >> Coin >> P.cash_box,
+            T.a >> P.signal,
+            P.signal >> T.b,
+            P.storage >> Packet >> T.b,
+            T.b >> Packet >> P.compartment,
+            P.compartment >> Packet >> T.take_packet,
+        }
+
+        clusters = {
+            "": {
+                T.insert_coin,
+                P.coin_slot,
+                T.a,
+                P.signal,
+                T.b,
+                P.compartment,
+                T.take_packet,
+            }
+        }
+
+        example_markings = {
+            "Figure 1.7: The cookie vending machine with the option of returning coins": {
+                P.storage: Packet() * 5,
+            },
+            "Figure 1.8: Two options: `return coin` and `a`": {
+                P.coin_slot: {Coin()},
+                P.storage: Packet() * 5,
+            },
+            "Figure 1.9: Reachable marking of the cookie vending machine": {
+                P.cash_box: Coin() * 5,
+                P.signal: {Abstract()},
+            },
+        }
diff --git a/examples/understanding_petri_nets/figure_1_10_11.py b/examples/understanding_petri_nets/figure_1_10_11.py
new file mode 100644
index 0000000..c245e15
--- /dev/null
+++ b/examples/understanding_petri_nets/figure_1_10_11.py
@@ -0,0 +1,78 @@
+from carladam import Abstract, Annotate, Color, PetriNet, Place, TransformEach, Transition
+
+Coin = Color("🪙")
+Packet = Color("📦")
+Counter = Color("🧮")
+
+
+def x_minus_1(token):
+    return token.replace(x=token.data["x"] - 1)
+
+
+def counter_x_gt_0(inputs):
+    counters = (token for token in inputs if token.color == Counter)
+    return all(counter.data["x"] > 0 for counter in counters)
+
+
+class UnderstandingPetriNetsFigure_1_10_11(PetriNet):  # noqa
+    """Understanding Petri Nets: Figures 1.10, 1.11"""
+
+    class Structure:
+        class P:
+            cash_box = Place()
+            coin_slot = Place()
+            compartment = Place()
+            counter = Place()
+            signal = Place()
+            storage = Place()
+
+        class T:
+            a = Transition(
+                guard=counter_x_gt_0,
+                annotation="x > 0",
+                fn=[Coin.produce(), Abstract.produce(), Counter.passthrough()],
+            )
+            b = Transition(fn=[Abstract.produce(), Packet.produce()])
+            insert_coin = Transition(fn=Coin.produce())
+            return_coin = Transition()
+            take_packet = Transition()
+
+        arcs = {
+            P.coin_slot >> Coin >> T.a,
+            P.coin_slot >> Coin >> T.return_coin,
+            P.compartment >> Packet >> T.take_packet,
+            P.counter >> Counter >> Annotate("x") >> T.a,
+            P.signal >> T.b,
+            P.storage >> Packet >> T.b,
+            T.a >> Coin >> P.cash_box,
+            T.a >> Counter >> Annotate("x-1") >> TransformEach(x_minus_1) >> P.counter,
+            T.a >> P.signal,
+            T.b >> Packet >> P.compartment,
+            T.insert_coin >> Coin >> P.coin_slot,
+        }
+
+        example_markings = {
+            "Figure 1.10: Addition of a counter": {
+                P.coin_slot: {Coin()},
+                P.counter: {Counter(x=5)},
+                P.storage: Packet() * 5,
+            },
+            "Figure 1.11: After the occurrence of a": {
+                P.counter: {Counter(x=4)},
+                P.storage: Packet() * 5,
+                P.cash_box: {Coin()},
+                P.signal: {Abstract()},
+            },
+        }
+
+        clusters = {
+            "": {
+                T.insert_coin,
+                P.coin_slot,
+                T.a,
+                P.signal,
+                T.b,
+                P.compartment,
+                T.take_packet,
+            },
+        }
diff --git a/examples/understanding_petri_nets/vending_machine.py b/examples/understanding_petri_nets/figure_1_12.py
similarity index 76%
rename from examples/understanding_petri_nets/vending_machine.py
rename to examples/understanding_petri_nets/figure_1_12.py
index 4ea02a6..e05dbcc 100644
--- a/examples/understanding_petri_nets/vending_machine.py
+++ b/examples/understanding_petri_nets/figure_1_12.py
@@ -1,10 +1,8 @@
-# Internal imports
-from carladam import Abstract, Annotate, Color, PetriNet, Place, Token, TransformEach, Transition
-
+from carladam import Abstract, Annotate, Color, PetriNet, Place, TransformEach, Transition
 
 Coin = Color("🪙")
 Packet = Color("📦")
-Counter = Color("🖩")
+Counter = Color("🧮")
 
 
 def x_minus_1(token):
@@ -16,8 +14,12 @@ def counter_x_gt_0(inputs):
     return all(counter.data["x"] > 0 for counter in counters)
 
 
-class UnderstandingPetriNetsVendingMachine(PetriNet):
-    """Full vending machine example from figure 1.12"""
+class UnderstandingPetriNetsFigure_1_12(PetriNet):  # noqa
+    """
+    Understanding Petri Nets: Figure 1.12
+
+    "At most one coin in the coin slot and at most one signal pending."
+    """
 
     class Structure:
         class P:
@@ -61,22 +63,17 @@ class UnderstandingPetriNetsVendingMachine(PetriNet):
         }
 
         example_markings = {
-            "Start here": {
-                P.counter: {Counter(x=5)},
-                P.insertion_possible: {Token()},
-                P.no_signal: {Token()},
-                P.storage: {Packet(), Packet(), Packet(), Packet(), Packet()},
-            },
-            "Coin inserted": {
-                P.coin_slot: {Coin()},
+            "Figure 1.12: At most one coin in the coin slot and it most one signal pending": {},
+            "Initialized vending machine": {
                 P.counter: {Counter(x=5)},
-                P.no_signal: {Token()},
-                P.storage: {Packet(), Packet(), Packet(), Packet(), Packet()},
+                P.insertion_possible: {Abstract()},
+                P.no_signal: {Abstract()},
+                P.storage: Packet() * 5,
             },
         }
 
         clusters = {
-            "Happy Path": {
+            "": {
                 T.insert_coin,
                 P.coin_slot,
                 T.a,
@@ -84,5 +81,9 @@ class UnderstandingPetriNetsVendingMachine(PetriNet):
                 T.b,
                 P.compartment,
                 T.take_packet,
-            }
+            },
+            "Coin return": {
+                T.return_coin,
+                P.insertion_possible,
+            },
         }


Test patch:
diff --git a/tests/test_token.py b/tests/test_token.py
index 123e31b..da0bfc5 100644
--- a/tests/test_token.py
+++ b/tests/test_token.py
@@ -70,3 +70,15 @@ def test_one():
     assert one()({t0}) == t0
     with pytest.raises(ValueError):
         one()({t0, t1})
+
+
+def test_token_multiply():
+    tokens = Token(data={"foo": "bar"}) * 3
+    assert len(tokens) == 3
+    assert all(t.data == {"foo": "bar"} for t in tokens)
+
+
+@pytest.mark.parametrize("value", [1.2, "string", [], {}, None])
+def test_token_multiply_not_int(value):
+    with pytest.raises(TypeError):
+        Token() * value  # noqa