Prechádzať zdrojové kódy

refactor of dynamic models and added the option to generate multiple models in one generation cycle, updated and improved cli

Snej69420 1 týždeň pred
rodič
commit
96e895ca00

+ 77 - 103
devstone/pythonpdevs/devstone.py

@@ -43,8 +43,6 @@ class DelayedAtomic(AtomicDEVS):
 
         # for DSDEVS LI2HI
         if len(self.out_ports) == 1 and self.out_ports[0].outline != []:
-            # print(f"{self.name}: should be last call!!")
-            # print("Send to ", self.out_ports[0].outline)
             return {self.out_ports[0]: [0]}
         return {}
 
@@ -168,7 +166,6 @@ class DEVStoneWrapper(CoupledDEVS, ABC):
         result += f"\tCoupled_{self.depth}[shape=box, label=\"C_{self.depth}\"];\n"
         coupled = self.component_set[0]
         result += f"\t{coupled.name}[shape=box, label=\"{'C_' + '_'.join(coupled.name.split('_')[1:3])}\"];\n"
-
         for model in self.models:
             result += f"\t{model.name}[shape=ellipse, label=\"{'A_' + '_'.join(model.name.split('_')[1:3])}\"];\n"
 
@@ -380,13 +377,13 @@ class HI2LI(DEVStoneWrapper):
         return HI2LI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
                   prep_time=self.prep_time, stats=self.stats)
 
-TRANSITION_TIME = 0
 
 class DynamicGenerator(AtomicDEVS):
-    def __init__(self, name: str, period: float = 1, repeat: int = 1):
+    def __init__(self, name: str, period: float = 1, repeat: int = 1, num: int = 1):
         super().__init__(name)
         self.period = period
         self.repeat = repeat
+        self.number = num # how many models need to be generated per cycle
 
         self._counter = 0
 
@@ -405,147 +402,124 @@ class DynamicGenerator(AtomicDEVS):
         # every time TA elapses, this will be called on the atomic:
         # set a flag the parent coupled can read
         # increment counter and request modelTransition (return True)
-        global TRANSITION_TIME
-        start = time.time()
         if self._counter >= self.repeat:
-            TRANSITION_TIME += time.time() - start
             return False
         self._counter += 1
         state["generate"] = True
+        state["create"] = self.number
         state["created"] = 0
-        TRANSITION_TIME += time.time() - start
         return True
 
+class DynamicDEVStoneWrapper(DEVStoneWrapper, ABC):
+    def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float,
+                 add_atomic_out_ports: bool = False, /, prep_time=0, stats=False, root=False, mode: str = "", number: int = 1, gen_mode: str="uniform"):
+        if number <= 0:
+            raise ValueError("number of models to be generated must be greater than zero")
 
-class dLI(DEVStoneWrapper):
-    """
-    A LI model which grows in width
-    """
-    def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, generator_mode: str = "uniform", root=True, prep_time=0, stats=False):
-        super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=False, prep_time=prep_time,
-                         stats=stats, mode="LI-Dynamic", dynamic=True)
+        # makes it easier to reason about width should be divisible by the number provided
+        # but the width provided also includes a coupled model so the actual width to be generated
+        # would be width - 1, this mitigates that
+        if number > 1 and root:
+            width += 1
 
         self.root = root
-        for idx in range(1, len(self.component_set)):
-            assert isinstance(self.component_set[idx], AtomicDEVS)
-            self.connectPorts(self.i_in, self.component_set[idx].i_in)
-
-        self.created = 0
+        self.mode = mode
+        self.created = 0 # keeps track of how many atomic models have been created
         self.current_width = 1
-        self.max_gen = width - 1 # dynamically generate atomic components until width is reached excludes the coupled model
+        self.number = number
+        self.max_gen = (width - 1) // self.number # dynamically generate atomic components until width is reached excludes the coupled model
+
+        super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports, prep_time=prep_time,
+                         stats=stats, mode=mode, dynamic=True)
         if self.depth == 1:
-            self.generator = self.addSubModel(DynamicGenerator(self.name + "_gen", repeat=self.max_gen))
+            self.generator = self.addSubModel(DynamicGenerator(self.name + "_gen", repeat=self.max_gen, num=self.number))
 
-    def gen_coupled(self):
-        return dLI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
-                   prep_time=self.prep_time, stats=self.stats, root=False)
+    def createAtomic(self):
+        name = f"Atomic_0_{self.current_width}"
+        if self.depth > 1:
+            name = f"Atomic_{self.depth-1}_{self.current_width}"
+
+        if self.stats:
+            atomic = DelayedAtomicStats(name, self.int_delay, self.ext_delay,
+                                          add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time,
+                                          mode=self.mode)
+        else:
+            atomic = DelayedAtomic(name, self.int_delay, self.ext_delay,
+                                     add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time,
+                                     mode=self.mode)
+
+        self.models.append(self.addSubModel(atomic))
+        return atomic
 
     def modelTransition(self, state):
         if state.get("generate", True) and self.depth == 1:
             return True
 
         if state.get("generate", True) and self.depth > 1:
-            name = "Atomic_%d_%d" % (self.depth - 1, self.current_width) if self.depth > 1 else "Atomic_0_%d" % self.current_width
-            if self.stats:
-                new_atom = DelayedAtomicStats(name, self.int_delay, self.ext_delay,
-                                              add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
-            else:
-                new_atom = DelayedAtomic(name, self.int_delay, self.ext_delay,
-                                         add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
-
-            self.models.append(self.addSubModel(new_atom))
-            self.connectPorts(self.i_in, new_atom.i_in)
-
-            self.current_width += 1
-            if self.current_width > self.width:
-                raise RuntimeError(f"The width has grown beyond what was specified!"
+            number = state.get("create")
+            for _ in range(number):
+                atom = self.createAtomic()
+                self.connectPorts(self.i_in, atom.i_in)
+
+                if len(self.models) > 1:
+                    prev = self.models[-2] # new model was already added so the previous one isn't the last in the list anymore
+                    if isinstance(prev, AtomicDEVS) and hasattr(prev, "o_out"):
+                        self.connectPorts(prev.o_out, atom.i_in)
+                self.current_width += 1
+
+                if self.current_width > self.width:
+                    raise RuntimeError(f"The width has grown beyond what was specified!"
                                    f"\n current width: {self.current_width}"
                                    f"\n specified width: {self.width}")
 
             # enables visualisation of each level (coupled model)
-            # if self.current_width == self.width:
-            #     self.dot(f"dLI_{self.depth}")
+                # if self.current_width == self.width:
+                #     self.dot(f"dLI_{self.depth}")
+
             if not self.root:
-                state["created"] += 1
+                state["created"] += number
                 return True
-            self.created += state["created"] + 1
-            # print(f"Created {self.created} atomic models")
+            self.created += state["created"] + number
             return False
         return False
 
 
-class dHI(DEVStoneWrapper):
+class dLI(DynamicDEVStoneWrapper):
+    """
+    A LI model which grows in width
+    """
+    def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, /, prep_time=0, stats=False, root=True, number: int = 1, gen_mode: str = "uniform"):
+        super().__init__(name, depth, width, int_delay, ext_delay, False, prep_time, stats, mode="LI-Dynamic", root=root, number=number, gen_mode=gen_mode)
+
+        for idx in range(1, len(self.models)):
+            assert isinstance(self.component_set[idx], AtomicDEVS)
+            self.connectPorts(self.i_in, self.component_set[idx].i_in)
+
+    def gen_coupled(self):
+        return dLI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
+                   prep_time=self.prep_time, stats=self.stats, root=False, number=self.number)
+
+
+class dHI(DynamicDEVStoneWrapper):
     """
     A HI model which grows in width
     """
-    def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, generator_mode: str = "uniform", root=True, prep_time=0, stats=False):
-        super().__init__(name, depth, width, int_delay, ext_delay, add_atomic_out_ports=True, prep_time=prep_time,
-                         stats=stats, mode="HI-Dynamic", dynamic=True)
+    def __init__(self, name: str, depth: int, width: int, int_delay: float, ext_delay: float, /, prep_time=0, stats=False, root=True, number: int = 1, gen_mode: str = "uniform"):
+        super().__init__(name, depth, width, int_delay, ext_delay, True, prep_time, stats, mode="HI-Dynamic", root=root, number=number, gen_mode=gen_mode)
 
-        self.root = root
-        if len(self.component_set) > 1:
+        if len(self.models) > 1:
             assert isinstance(self.component_set[-1], AtomicDEVS)
             self.connectPorts(self.i_in, self.component_set[-1].i_in)
 
-        for idx in range(1, len(self.component_set) - 1):
+        for idx in range(1, len(self.models) - 1):
             assert isinstance(self.component_set[idx], AtomicDEVS)
             self.connectPorts(self.component_set[idx].o_out, self.component_set[idx + 1].i_in)
             self.connectPorts(self.i_in, self.component_set[idx].i_in)
 
-        self.created = 0
-        self.current_width = 1
-        self.max_gen = width - 1 # dynamically generate atomic components until width is reached excludes the coupled model
-        if self.depth == 1:
-            self.generator = self.addSubModel(DynamicGenerator(self.name + "_gen", repeat=self.max_gen))
 
     def gen_coupled(self):
         return dHI("Coupled_%d" % (self.depth - 1), self.depth - 1, self.width, self.int_delay, self.ext_delay,
-                   prep_time=self.prep_time, stats=self.stats, root=False)
-
-    def modelTransition(self, state):
-        start = time.time()
-        global TRANSITION_TIME
-        if state.get("generate", True) and self.depth == 1:
-            TRANSITION_TIME += time.time() - start
-            return True
-
-        if state.get("generate", True) and self.depth > 1:
-            name = "Atomic_%d_%d" % (self.depth - 1, self.current_width) if self.depth > 1 else "Atomic_0_%d" % self.current_width
-            if self.stats:
-                new_atom = DelayedAtomicStats(name, self.int_delay, self.ext_delay,
-                                              add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
-            else:
-                new_atom = DelayedAtomic(name, self.int_delay, self.ext_delay,
-                                         add_out_port=self.add_atomic_out_ports, prep_time=self.prep_time, mode="LI-Dynamic")
-
-            self.models.append(self.addSubModel(new_atom))
-            self.connectPorts(self.i_in, new_atom.i_in)
-            if len(self.models) > 1:
-                prev = self.models[-2]
-                if isinstance(prev, AtomicDEVS) and hasattr(prev, "o_out"):
-                    self.connectPorts(prev.o_out, new_atom.i_in)
-            self.current_width += 1
-            if self.current_width > self.width:
-                TRANSITION_TIME += time.time() - start
-                raise RuntimeError(f"The width has grown beyond what was specified!"
-                                   f"\n current width: {self.current_width}"
-                                   f"\n specified width: {self.width}")
-
-            # enables visualisation of each level (coupled model)
-            # if self.current_width == self.width:
-            #     self.dot(f"dLI_{self.depth}")
-
-            if not self.root:
-                state["created"] += 1
-                TRANSITION_TIME += time.time() - start
-                return True
-            self.created += state["created"] + 1
-            # print(f"Created {self.created} atomic models")
-            TRANSITION_TIME += time.time() - start
-            # print(f"TRANSITION TIME: {TRANSITION_TIME}")
-            return False
-        return False
-
+                   prep_time=self.prep_time, stats=self.stats, root=False, number=self.number)
 
 if __name__ == '__main__':
     import sys

+ 10 - 9
devstone/pythonpdevs/main.py

@@ -49,6 +49,7 @@ def parse_args():
 
     parser.add_argument('-C', '--classic', action="store_true", default=False, help='Use classic DEVS simulator')
     parser.add_argument('-D', '--dynamic', action="store_true", default=False, help='Enables Dynamic Structure DEVS')
+    parser.add_argument('-n', '--number', required=True, default=1, type=int, help='Number of models to be generated within a cycle in a single coupled model')
     parser.add_argument("-g", "--gen-mode", choices=GEN_METHODS, help="Determines the spread of model creation.")
     args = parser.parse_args()
 
@@ -66,21 +67,21 @@ if __name__ == '__main__':
 
     start_time = time.time()
     if args.model_type == "LI":
-        devstone_model = LI("LI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, prep_time=1)
+        devstone_model = LI("LI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, 1)
     elif args.model_type == "HI":
-        devstone_model = HI("HI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, prep_time=1)
+        devstone_model = HI("HI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, 1)
     elif args.model_type == "HO":
-        devstone_model = HO("HO_root", args.depth, args.width, args.int_cycles, args.ext_cycles, prep_time=1)
+        devstone_model = HO("HO_root", args.depth, args.width, args.int_cycles, args.ext_cycles, 1)
     elif args.model_type == "HOmod":
-        devstone_model = HOmod("HOmod_root", args.depth, args.width, args.int_cycles, args.ext_cycles, prep_time=1)
+        devstone_model = HOmod("HOmod_root", args.depth, args.width, args.int_cycles, args.ext_cycles, 1)
     elif args.model_type == "LI2HI":
-        devstone_model = LI2HI("LI2HI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, prep_time=1)
+        devstone_model = LI2HI("LI2HI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, 1)
     elif args.model_type == "HI2LI":
-        devstone_model = HI2LI("HI2LI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, prep_time=1)
+        devstone_model = HI2LI("HI2LI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, 1)
     elif args.model_type == "dLI":
-        devstone_model = dLI("dLI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, args.gen_mode, prep_time=1)
+        devstone_model = dLI("dLI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, 1, number=args.number, gen_mode=args.gen_mode)
     elif args.model_type == "dHI":
-        devstone_model = dHI("dHI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, args.gen_mode, prep_time=1)
+        devstone_model = dHI("dHI_root", args.depth, args.width, args.int_cycles, args.ext_cycles, 1, number=args.number, gen_mode=args.gen_mode)
 
     env = DEVStoneEnvironment("DEVStoneEnvironment", devstone_model)
     model_created_time = time.time()
@@ -113,7 +114,7 @@ if __name__ == '__main__':
     # sortby = pstats.SortKey.CUMULATIVE
     # ps = pstats.Stats(pr, stream=s).sort_stats(sortby)
     # ps.print_stats()
-    # print(s.getvalue())
+    # print(s.getvalue()[:2500])
 
     # devstone_model.dot(f"{args.model_type}-after")
 

+ 96 - 24
devstone/testPyPDEVS.py

@@ -26,10 +26,10 @@ TIME_PATTERN = re.compile(
     re.S
 )
 
-def run_devstone(model_type, depth, width, minimal=False, classic=False, dynamic=False, gen_mode="uniform"):
+def run_devstone(model_type, depth, width, mode="parallel", out=False, *, classic=False, number=1, gen_mode="uniform"):
     """Runs the devstone_driver.py script and returns timing results as floats."""
     SCRIPT_PATH = PyPDEVS
-    if minimal:
+    if mode == "minimal":
         SCRIPT_PATH = MinimalPyPDEVS
 
     cmd = [
@@ -40,14 +40,20 @@ def run_devstone(model_type, depth, width, minimal=False, classic=False, dynamic
         "-i", str(INT_CYCLES),
         "-e", str(EXT_CYCLES),
     ]
-    if dynamic and not minimal:
+    if mode == "dynamic":
         cmd.extend(["-D"])
+        if classic:
+            cmd.extend(["-C"])
+        cmd.extend(["-n", str(number)])
         cmd.extend(["-g", gen_mode])
-    if classic and not minimal:
+    if mode == "classic":
         cmd.extend(["-C"])
+
     result = subprocess.run(cmd, capture_output=True, text=True)
     output = result.stdout.strip()
-    # print(output)
+    if out:
+        print(output)
+
     errors = result.stderr.strip()
     match = TIME_PATTERN.search(output)
 
@@ -60,33 +66,85 @@ def run_devstone(model_type, depth, width, minimal=False, classic=False, dynamic
     print(errors)
     return None, None, None, output
 
-def ensureDS(args):
-    intersection = list(set(args.model_structure) & set(DS_MODEL_TYPES))
-    if intersection and not args.dynamic:
-        raise ParserError(f"Dynamic Models ({DS_MODEL_TYPES}) can only be used with --dynamic (-D)")
+def Minimal(subparsers):
+    minimal = subparsers.add_parser("minimal", help="Minimal DEVS kernel")
+    minimal.set_defaults(mode="minimal")
+    minimal.add_argument(
+        "-ms", "--model-structure",
+        nargs="+",
+        choices=MODEL_TYPES,
+        default=MODEL_TYPES
+    )
+
+def Classic(subparsers):
+    classic = subparsers.add_parser("classic", help="Classic DEVS Simulator")
+    classic.set_defaults(mode="classic")
+    classic.add_argument(
+        "-ms", "--model-structure",
+        nargs="+",
+        choices=MODEL_TYPES,
+        default=MODEL_TYPES
+    )
+
+def Parallel(subparsers):
+    parallel = subparsers.add_parser("parallel", help="Parallel DEVS")
+    parallel.set_defaults(mode="parallel")
+    parallel.add_argument(
+        "-ms", "--model-structure",
+        nargs="+",
+        choices=MODEL_TYPES,
+        default=MODEL_TYPES
+    )
+
+def Dynamic(subparsers):
+    dynamic = subparsers.add_parser("dynamic", help="Dynamic Structure DEVS")
+    dynamic.set_defaults(mode="dynamic")
+    dynamic.add_argument(
+        "-ms", "--model-structure",
+        nargs="+",
+        choices=MODEL_TYPES + DS_MODEL_TYPES,
+        default=MODEL_TYPES + DS_MODEL_TYPES
+    )
+    dynamic.add_argument(
+        "-C", "--classic",
+        action="store_true",
+        default=False,
+        help="Selects the Classic DEVS Simulator"
+    )
+    dynamic.add_argument(
+        "-n", "--number",
+        default=1,
+        type=int,
+        help="Number of models to be generated in a cycle per coupled model")
+    dynamic.add_argument(
+        "-g", "--gen-mode",
+        choices=GEN_METHODS,
+        default="uniform"
+    )
 
 def parse_args():
     parser = ArgumentParser()
     parser.add_argument("-p", "--path", type=str, default="./Results-PyPDEVS/", help="Path to the results directory")
     parser.add_argument("-f", '--filename', type=str, default="", help="Name of the output file")
 
-    parser.add_argument("-M", "--minimal", action="store_true", default=False, help="Uses the minimal DEVS kernel. (Less features => less overhead)")
-    parser.add_argument("-C", "--classic", action="store_true", default=False, help="Selects the Classic DEVS Simulator")
-    parser.add_argument("-D", "--dynamic", action="store_true", default=False, help="Enables Dynamic Structure DEVS")
-    parser.add_argument("-g", "--gen-mode", choices=GEN_METHODS, default="uniform", help="Determines the spread of model creation.")
-
-    parser.add_argument("-ms", "--model-structure", nargs="+", default=MODEL_TYPES+DS_MODEL_TYPES, choices=MODEL_TYPES+DS_MODEL_TYPES, help="Select one or more model structures")
-
     parser.add_argument("-md", "--max-depth", default=8, type=int, help="Max depth for the generated models")
-    parser.add_argument("-ds", "--depth-stepsize", default=1, type=int, help="Determines for which depths a model is generated.")
+    parser.add_argument("-ds", "--depth-stepsize", default=1, type=int,
+                        help="Determines for which depths a model is generated.")
 
     parser.add_argument("-mw", "--max-width", default=8, type=int, help="Max width for the generated models")
-    parser.add_argument("-ws", "--width-stepsize", default=1, type=int, help="Determines for which depths a model is generated.")
+    parser.add_argument("-ws", "--width-stepsize", default=1, type=int,
+                        help="Determines for which depths a model is generated.")
 
     parser.add_argument("-r", "--repetitions", default=1, type=int, help="The number of times a simulation is repeated")
+    parser.add_argument("-O", "--output", action="store_true", help="Print Simulation Outputs")
+
+    subparsers = parser.add_subparsers(dest="mode")
+    Minimal(subparsers)
+    Parallel(subparsers)
+    Classic(subparsers)
+    Dynamic(subparsers)
 
     args = parser.parse_args()
-    ensureDS(args)
     return args
 
 if __name__ == "__main__":
@@ -101,7 +159,10 @@ if __name__ == "__main__":
         raise RuntimeError("Invalid width stepsize.")
 
     fieldnames = ["model_type", "depth", "width", "run",
-                  "creation_time", "setup_time", "simulation_time", "generator_mode"]
+                  "creation_time", "setup_time", "simulation_time"]
+    if args.mode == "dynamic":
+        fieldnames += ["number", "generator_mode"]
+
 
     for model_type in args.model_structure:
         model_filename = f"{args.path}{model_type}"
@@ -116,12 +177,20 @@ if __name__ == "__main__":
             for depth in range(d[1], d[0] + 1, d[1]):
                 for width in range(w[1], w[0] + 1, w[1]):
                     for run in range(args.repetitions):
+                        extra = {}
+                        if args.mode == "dynamic":
+                            extra["classic"] = args.classic
+                            extra["gen_mode"] = args.gen_mode
+                            extra["number"] = args.number
+
                         creation, setup, sim, output = run_devstone(
                             model_type, depth, width,
-                            args.minimal, args.classic, args.dynamic, args.gen_mode
+                            args.mode, args.output,
+                            **extra
                         )
+
                         if creation is not None:
-                            writer.writerow({
+                            row = {
                                 "model_type": model_type,
                                 "depth": depth,
                                 "width": width,
@@ -129,8 +198,11 @@ if __name__ == "__main__":
                                 "creation_time": creation,
                                 "setup_time": setup,
                                 "simulation_time": sim,
-                                "generator_mode": args.gen_mode
-                            })
+                            }
+                            if args.mode == "dynamic":
+                                row["number"] = args.number
+                                row["generator_mode"] = args.gen_mode
+                            writer.writerow(row)
                             csvfile.flush()
         print(f"✅ Saved {model_type} results to {model_filename}")