testPyPDEVS.py 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. #!/usr/bin/env python3
  2. import os
  3. import subprocess
  4. import csv
  5. import re
  6. from argparse import ArgumentParser, Action
  7. from pathlib import Path
  8. from itertools import product
  9. from datetime import datetime
  10. from pandas.errors import ParserError
  11. MODEL_TYPES = ("LI", "HI", "HO", "HOmod")
  12. DS_MODEL_TYPES = ("LI2HI", "HI2LI", "LI-HI", "dLI", "dHI")
  13. GEN_METHODS = ("start", "uniform", "end")
  14. INT_CYCLES = 0
  15. EXT_CYCLES = 0
  16. PyPDEVS = "./pythonpdevs/main.py"
  17. MinimalPyPDEVS = "./pythonpdevs-minimal/main.py"
  18. # Regex to capture times from script output
  19. TIME_PATTERN = re.compile(
  20. r"Model creation time: ([\d.eE+-]+).*"
  21. r"Engine setup time: ([\d.eE+-]+).*"
  22. r"Simulation time: ([\d.eE+-]+)",
  23. re.S
  24. )
  25. def run_devstone(model_type, depth, width, mode, out, memory, *, classic=False, number=1, gen_mode="uniform"):
  26. """Runs the devstone_driver.py script and returns timing results as floats."""
  27. SCRIPT_PATH = PyPDEVS
  28. if mode == "minimal":
  29. SCRIPT_PATH = MinimalPyPDEVS
  30. cmd = [
  31. "uv", "run", SCRIPT_PATH,
  32. "-m", model_type,
  33. "-d", str(depth),
  34. "-w", str(width),
  35. "-i", str(INT_CYCLES),
  36. "-e", str(EXT_CYCLES),
  37. ]
  38. if mode == "dynamic":
  39. cmd.extend(["-D"])
  40. if classic:
  41. cmd.extend(["-C"])
  42. cmd.extend(["-n", str(number)])
  43. cmd.extend(["-g", gen_mode])
  44. if mode == "classic":
  45. cmd.extend(["-C"])
  46. full_cmd = " ".join(cmd)
  47. if memory:
  48. Path("/".join(memory["file"].split("/")[0:-1])).mkdir(parents=True, exist_ok=True)
  49. full_cmd = (
  50. f"{full_cmd} & "
  51. f"PID=$!; "
  52. f"python -m procpath record -i {memory['interval']} -f stat -d {memory['file']} "
  53. f"--stop-without-result \"$..children[?(@.stat.pid == $PID)]\""
  54. )
  55. result = subprocess.run(full_cmd, shell=True, capture_output=True, text=True)
  56. cout = result.stdout.strip()
  57. if out:
  58. print(cout)
  59. cerr = result.stderr.strip()
  60. match = TIME_PATTERN.search(cout)
  61. if match:
  62. creation, setup, sim = map(float, match.groups())
  63. return creation, setup, sim
  64. print("Failed to run: ", cmd)
  65. print(cout)
  66. print(cerr)
  67. return None, None, None
  68. def defaultArguments(parser):
  69. parser.add_argument("-p", "--path", type=str, default="./Results-PyPDEVS", help="Path to the results directory")
  70. parser.add_argument("-f", '--filename', type=str, default="", help="Name of the output file")
  71. parser.add_argument("-a", "--append", action="store_true", help="Append to the output file instead of overwriting.")
  72. parser.add_argument("-md", "--max-depth", default=8, type=int, help="Max depth for the generated models")
  73. parser.add_argument("-ds", "--depth-stepsize", type=int, help="Determines for which depths a model is generated.")
  74. parser.add_argument("-mw", "--max-width", default=8, type=int, help="Max width for the generated models")
  75. parser.add_argument("-ws", "--width-stepsize", type=int, help="Determines for which depths a model is generated.")
  76. parser.add_argument("-r", "--repetitions", default=1, type=int, help="The number of times a simulation is repeated")
  77. parser.add_argument("-O", "--output", action="store_true", help="Print Simulation Outputs")
  78. parser.add_argument("-M", "--memory", action="store_true", help="Measures the memory usage of a simulation.")
  79. parser.add_argument("-i", "--interval", default=1.0, type=float, help="The interval at which measurements are taken.")
  80. def Minimal(subparsers):
  81. minimal = subparsers.add_parser("minimal", help="Minimal DEVS kernel")
  82. minimal.set_defaults(mode="minimal")
  83. defaultArguments(minimal)
  84. minimal.add_argument(
  85. "-ms", "--model-structure",
  86. nargs="+",
  87. choices=MODEL_TYPES,
  88. default=MODEL_TYPES
  89. )
  90. def Classic(subparsers):
  91. classic = subparsers.add_parser("classic", help="Classic DEVS Simulator")
  92. classic.set_defaults(mode="classic")
  93. defaultArguments(classic)
  94. classic.add_argument(
  95. "-ms", "--model-structure",
  96. nargs="+",
  97. choices=MODEL_TYPES,
  98. default=MODEL_TYPES
  99. )
  100. def Parallel(subparsers):
  101. parallel = subparsers.add_parser("parallel", help="Parallel DEVS")
  102. parallel.set_defaults(mode="parallel")
  103. defaultArguments(parallel)
  104. parallel.add_argument(
  105. "-ms", "--model-structure",
  106. nargs="+",
  107. choices=MODEL_TYPES,
  108. default=MODEL_TYPES
  109. )
  110. def Dynamic(subparsers):
  111. dynamic = subparsers.add_parser("dynamic", help="Dynamic Structure DEVS")
  112. dynamic.set_defaults(mode="dynamic")
  113. defaultArguments(dynamic)
  114. dynamic.add_argument(
  115. "-ms", "--model-structure",
  116. nargs="+",
  117. choices=MODEL_TYPES + DS_MODEL_TYPES,
  118. default=MODEL_TYPES + DS_MODEL_TYPES
  119. )
  120. dynamic.add_argument(
  121. "-C", "--classic",
  122. action="store_true",
  123. default=False,
  124. help="Selects the Classic DEVS Simulator"
  125. )
  126. dynamic.add_argument(
  127. "-n", "--number",
  128. default=1,
  129. type=int,
  130. help="Number of models to be generated in a cycle per coupled model")
  131. dynamic.add_argument(
  132. "-g", "--gen-mode",
  133. choices=GEN_METHODS,
  134. default="uniform"
  135. )
  136. def parse_args():
  137. parser = ArgumentParser()
  138. subparsers = parser.add_subparsers(dest="mode")
  139. Minimal(subparsers)
  140. Parallel(subparsers)
  141. Classic(subparsers)
  142. Dynamic(subparsers)
  143. args = parser.parse_args()
  144. if args.depth_stepsize is None:
  145. args.depth_stepsize = args.max_depth
  146. if args.width_stepsize is None:
  147. args.width_stepsize = args.max_width
  148. return args
  149. def run(writer, csvfile, model_type, d, w, args, mem_cmd, extra):
  150. """Orchestrates the repeated simulation runs."""
  151. for depth in range(d[1], d[0] + 1, d[1]):
  152. for width in range(w[1], w[0] + 1, w[1]):
  153. for run in range(args.repetitions):
  154. creation, setup, sim = run_devstone(
  155. model_type, depth, width,
  156. args.mode, args.output,
  157. mem_cmd,
  158. **extra
  159. )
  160. # Only write to CSV if a writer was provided
  161. if writer and creation is not None:
  162. row = {
  163. "model_type": model_type, "depth": depth, "width": width, "run": run,
  164. "creation_time": creation, "setup_time": setup, "simulation_time": sim,
  165. }
  166. if args.mode == "dynamic":
  167. row["number"] = args.number
  168. row["generator_mode"] = args.gen_mode
  169. writer.writerow(row)
  170. csvfile.flush()
  171. if __name__ == "__main__":
  172. args = parse_args()
  173. print(args)
  174. d = (args.max_depth, args.depth_stepsize)
  175. w = (args.max_width, args.width_stepsize)
  176. if d[0] % d[1] != 0:
  177. raise RuntimeError("Invalid depth stepsize.")
  178. if w[0] % w[1] != 0:
  179. raise RuntimeError("Invalid width stepsize.")
  180. fieldnames = ["model_type", "depth", "width", "run",
  181. "creation_time", "setup_time", "simulation_time"]
  182. mode = args.mode
  183. if mode == "dynamic":
  184. if args.classic:
  185. mode += "-classic"
  186. fieldnames += ["number", "generator_mode"]
  187. extra = {
  188. "classic": False if not hasattr(args, "classic") else args.classic,
  189. "gen_mode": "uniform" if not hasattr(args, "gen_mode") else args.gen_mode,
  190. "number": 1 if not hasattr(args, "number") else args.number,
  191. }
  192. for model_type in args.model_structure:
  193. model_time_file = f"{args.path}/Time/{mode}/{model_type}"
  194. model_memory_file = f"{args.path}/Memory/{mode}/{model_type}"
  195. if args.filename:
  196. suffix = f"_{args.filename}"
  197. model_time_file += suffix
  198. model_memory_file += suffix
  199. model_time_file += ".csv"
  200. model_memory_file += ".sqlite"
  201. mem_cmd = {"interval": args.interval, "file": model_memory_file} if args.memory else {}
  202. if not args.memory:
  203. file_mode = "a" if args.append else "w"
  204. write_header = not args.append or not os.path.exists(model_time_file)
  205. with open(model_time_file, file_mode, newline="") as csvfile:
  206. writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
  207. if write_header:
  208. writer.writeheader()
  209. run(writer, csvfile, model_type, d, w, args, mem_cmd, extra)
  210. print(f"✅ Saved timing results to {model_time_file}")
  211. else:
  212. print("📊 Measuring memory (Timing results will not be saved)...")
  213. run(None, None, model_type, d, w, args, mem_cmd, extra)
  214. print(f"✅ Saved memory database to {model_memory_file}")