#!/usr/bin/env python3 import subprocess import csv import re from argparse import ArgumentParser, Action from itertools import product from datetime import datetime from pandas.errors import ParserError MODEL_TYPES = ("LI", "HI", "HO", "HOmod") DS_MODEL_TYPES = ("LI2HI", "HI2LI", "LI-HI", "dLI", "dHI") GEN_METHODS = ("start", "uniform", "end") INT_CYCLES = 0 EXT_CYCLES = 0 PyPDEVS = "./pythonpdevs/main.py" MinimalPyPDEVS = "./pythonpdevs-minimal/main.py" # Regex to capture times from script output TIME_PATTERN = re.compile( r"Model creation time: ([\d.eE+-]+).*" r"Engine setup time: ([\d.eE+-]+).*" r"Simulation time: ([\d.eE+-]+)", re.S ) 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 mode == "minimal": SCRIPT_PATH = MinimalPyPDEVS cmd = [ "python3", SCRIPT_PATH, "-m", model_type, "-d", str(depth), "-w", str(width), "-i", str(INT_CYCLES), "-e", str(EXT_CYCLES), ] if mode == "dynamic": cmd.extend(["-D"]) if classic: cmd.extend(["-C"]) cmd.extend(["-n", str(number)]) cmd.extend(["-g", gen_mode]) if mode == "classic": cmd.extend(["-C"]) result = subprocess.run(cmd, capture_output=True, text=True) output = result.stdout.strip() if out: print(output) errors = result.stderr.strip() match = TIME_PATTERN.search(output) if match: creation, setup, sim = map(float, match.groups()) return creation, setup, sim, output print("Failed to run: ", cmd) print(output) print(errors) return None, None, None, output 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("-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("-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("-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() return args if __name__ == "__main__": args = parse_args() print(args) d = (args.max_depth, args.depth_stepsize) w = (args.max_width, args.width_stepsize) if d[0] % d[1] != 0: raise RuntimeError("Invalid depth stepsize.") if w[0] % w[1] != 0: raise RuntimeError("Invalid width stepsize.") fieldnames = ["model_type", "depth", "width", "run", "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}" if args.filename: model_filename += f"_{args.filename}" else: model_filename += ".csv" with open(model_filename, "w", newline="") as csvfile: writer = csv.DictWriter(csvfile, fieldnames=fieldnames) writer.writeheader() 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.mode, args.output, **extra ) if creation is not None: row = { "model_type": model_type, "depth": depth, "width": width, "run": run, "creation_time": creation, "setup_time": setup, "simulation_time": sim, } 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}")