|
|
@@ -5,79 +5,46 @@ import matplotlib.pyplot as plt
|
|
|
from pathlib import Path
|
|
|
from model_properties import model_properties
|
|
|
|
|
|
-MODEL_COLOR_FAMILIES = {
|
|
|
- "LI": "Greys",
|
|
|
- "HI": "Purples",
|
|
|
- "HO": "Blues",
|
|
|
- "HOmod": "Greens",
|
|
|
-
|
|
|
- "dLI": "YlOrRd",
|
|
|
- "dHI": "Oranges",
|
|
|
- "dHO": "Reds",
|
|
|
-
|
|
|
- "mdLI": "PuBu",
|
|
|
- "mdHI": "YlOrBr",
|
|
|
- "mdHO": "YlGn",
|
|
|
+# Dictionary mapping human-readable color names to styles
|
|
|
+PREDEFINED_STYLES = {
|
|
|
+ "blue": {"color": "tab:blue", "ls": "-", "marker": "o"},
|
|
|
+ "orange": {"color": "tab:orange", "ls": "--", "marker": "s"},
|
|
|
+ "green": {"color": "tab:green", "ls": "-.", "marker": "^"},
|
|
|
+ "red": {"color": "tab:red", "ls": ":", "marker": "D"},
|
|
|
+ "purple": {"color": "tab:purple", "ls": "-", "marker": "v"},
|
|
|
+ "brown": {"color": "tab:brown", "ls": "--", "marker": "X"},
|
|
|
}
|
|
|
|
|
|
-SIMULATOR_TRAITS = {
|
|
|
- "agents": {"label": "Agents", "shade_idx": 0, "marker": "^", "ls": "-"}, # Darkest, Triangle, Solid
|
|
|
- "pypdevs-parallel": {"label": "PyPDEVS (p)", "shade_idx": 1, "marker": "s", "ls": "-."},
|
|
|
- "pypdevs-minimal": {"label": "PyPDEVS (m)", "shade_idx": 2, "marker": "o", "ls": "--"},
|
|
|
- "pypdevs-classic": {"label": "PyPDEVS (c)", "shade_idx": 3, "marker": "D", "ls": ":"},
|
|
|
- "pypdevs-dynamic": {"label": "PyPDEVS (d)", "shade_idx": 1, "marker": "v", "ls": "-"},
|
|
|
- "pypdevs-dynamic-classic": {"label": "PyPDEVS (dc)", "shade_idx": 2, "marker": "p", "ls": "--"},
|
|
|
+SHORTHANDS = {
|
|
|
+ "b": "blue",
|
|
|
+ "o": "orange",
|
|
|
+ "g": "green",
|
|
|
+ "r": "red",
|
|
|
+ "p": "purple",
|
|
|
+ "br": "brown"
|
|
|
}
|
|
|
|
|
|
-def get_dynamic_colors(results, bw=False):
|
|
|
- """
|
|
|
- Generates dynamic, evenly spaced colormap values based on the number of simulators per model.
|
|
|
- """
|
|
|
- dynamic_colors = {}
|
|
|
+VALID_STYLE_INPUTS = list(PREDEFINED_STYLES.keys()) + list(SHORTHANDS.keys())
|
|
|
|
|
|
- # Find all unique model types currently loaded in the results
|
|
|
- model_types = set([m for _, _, m in results.values()])
|
|
|
+def get_style_config(user_input):
|
|
|
+ """Helper to resolve shorthands and handle case-insensitivity."""
|
|
|
+ normalized = user_input.lower()
|
|
|
+ key = SHORTHANDS.get(normalized, normalized)
|
|
|
+ return PREDEFINED_STYLES.get(key)
|
|
|
|
|
|
- for m_type in model_types:
|
|
|
- # Find all simulators for this specific model type
|
|
|
- sims = list(set([s for _, s, m in results.values() if m == m_type]))
|
|
|
-
|
|
|
- # Sort them by their predefined shade_idx so 'agents' always gets the darkest color
|
|
|
- sims.sort(key=lambda x: SIMULATOR_TRAITS.get(x, {}).get("shade_idx", 0))
|
|
|
-
|
|
|
- cmap_name = MODEL_COLOR_FAMILIES.get(m_type, "Greys")
|
|
|
- cmap = plt.get_cmap(cmap_name)
|
|
|
-
|
|
|
- # Matplotlib colormaps go from 0.0 (white/lightest) to 1.0 (black/darkest).
|
|
|
- # We sample between 0.9 and 0.4 so the colors don't become invisible on a white background.
|
|
|
- if len(sims) == 1:
|
|
|
- spacings = [0.8]
|
|
|
- else:
|
|
|
- spacings = np.linspace(0.9, 0.4, len(sims))
|
|
|
-
|
|
|
- # Assign the calculated color to the specific (model, simulator) combo
|
|
|
- for sim, space in zip(sims, spacings):
|
|
|
- dynamic_colors[(m_type, sim)] = 'black' if bw else cmap(space)
|
|
|
-
|
|
|
- return dynamic_colors
|
|
|
|
|
|
def clean_data(df):
|
|
|
- # Vectorized addition for total_time
|
|
|
df["total_time"] = (
|
|
|
df["simulation_time"]
|
|
|
+ df["creation_time"]
|
|
|
+ df["setup_time"]
|
|
|
)
|
|
|
|
|
|
- # Determine columns for grouping (exclude total_time itself from grouping if possible)
|
|
|
- # Usually, you want to group by parameters to find outliers within those specific settings
|
|
|
group_params = [c for c in ["depth", "width", "number"] if c in df.columns]
|
|
|
|
|
|
if not group_params:
|
|
|
return df
|
|
|
|
|
|
- # Vectorized IQR calculation using transform
|
|
|
- # transform('quantile') returns a Series aligned with the original DataFrame index
|
|
|
q1 = df.groupby(group_params)["total_time"].transform(lambda x: x.quantile(0.25))
|
|
|
q3 = df.groupby(group_params)["total_time"].transform(lambda x: x.quantile(0.75))
|
|
|
|
|
|
@@ -85,16 +52,12 @@ def clean_data(df):
|
|
|
lower = q1 - 1.5 * iqr
|
|
|
upper = q3 + 1.5 * iqr
|
|
|
|
|
|
- # Single-pass boolean indexing
|
|
|
return df[(df["total_time"] >= lower) & (df["total_time"] <= upper)]
|
|
|
|
|
|
+
|
|
|
def get_spaced_indices(series, n_markers):
|
|
|
- """
|
|
|
- Returns a list of indices that are roughly equidistant in terms of value,
|
|
|
- rather than index. This prevents marker clustering in dense data regions.
|
|
|
- """
|
|
|
if len(series) <= n_markers:
|
|
|
- return None # Plot all markers if we have few points
|
|
|
+ return None
|
|
|
|
|
|
values = series.values
|
|
|
min_v, max_v = values.min(), values.max()
|
|
|
@@ -102,35 +65,26 @@ def get_spaced_indices(series, n_markers):
|
|
|
if min_v == max_v:
|
|
|
return [0]
|
|
|
|
|
|
- # Create target values evenly spaced across the range
|
|
|
targets = np.linspace(min_v, max_v, n_markers)
|
|
|
-
|
|
|
indices = []
|
|
|
- # Find the index of the value closest to each target
|
|
|
- # Since 'grouped' is sorted by param, values are sorted.
|
|
|
- # We use searchsorted to find insertion points, then check neighbors.
|
|
|
import bisect
|
|
|
-
|
|
|
val_list = values.tolist()
|
|
|
|
|
|
for t in targets:
|
|
|
- # bisect finds the insertion point to maintain order
|
|
|
idx = bisect.bisect_left(val_list, t)
|
|
|
-
|
|
|
- # Check if the previous index is actually closer
|
|
|
if idx > 0:
|
|
|
if idx == len(val_list) or abs(val_list[idx - 1] - t) < abs(val_list[idx] - t):
|
|
|
idx = idx - 1
|
|
|
-
|
|
|
if idx < len(val_list):
|
|
|
indices.append(idx)
|
|
|
|
|
|
- # Deduplicate and sort
|
|
|
return sorted(list(set(indices)))
|
|
|
|
|
|
-def plot_time_vs_param(csv_files, param, fixed_param=None, fixed_value=None, max_value=0, o_path="", name="", bw=False):
|
|
|
- results = {}
|
|
|
+
|
|
|
+def plot_time_vs_param(csv_files, styles, param, fixed_param=None, fixed_value=None, max_value=0, o_path="", name=""):
|
|
|
+ results = []
|
|
|
models = []
|
|
|
+
|
|
|
for i, path in enumerate(csv_files):
|
|
|
df = pd.read_csv(path)
|
|
|
sim_id = df["simulator"].iloc[0] if "simulator" in df.columns else "None"
|
|
|
@@ -142,8 +96,8 @@ def plot_time_vs_param(csv_files, param, fixed_param=None, fixed_value=None, max
|
|
|
df = df[df[fixed_param] == fixed_value]
|
|
|
|
|
|
df = df.copy()
|
|
|
- if param == "#Models":
|
|
|
- df["#Models"] = df.apply(
|
|
|
+ if param == "Models":
|
|
|
+ df["Models"] = df.apply(
|
|
|
lambda row: model_properties(
|
|
|
str(row["model_type"]),
|
|
|
int(row["width"]),
|
|
|
@@ -157,36 +111,26 @@ def plot_time_vs_param(csv_files, param, fixed_param=None, fixed_value=None, max
|
|
|
df = df[df[param] <= max_value]
|
|
|
|
|
|
grouped = df.groupby(param, as_index=False)["total_time"].mean()
|
|
|
+ label = f"{model_type} ({sim_id})"
|
|
|
|
|
|
- # Determine the label
|
|
|
- sim_label = SIMULATOR_TRAITS.get(sim_id, {}).get("label", sim_id)
|
|
|
- label = f"{model_type} ({sim_label})"
|
|
|
+ style_input = styles[i] if i < len(styles) else list(PREDEFINED_STYLES.keys())[i % 6]
|
|
|
+ style = get_style_config(style_input)
|
|
|
|
|
|
- results[label] = grouped, sim_id, model_type
|
|
|
+ results.append((label, grouped, style))
|
|
|
|
|
|
- if not name:
|
|
|
- unique_models = "|".join(set(models))
|
|
|
- final_name = f"{unique_models}_over_{param}"
|
|
|
- else:
|
|
|
- final_name = name + "_" + param
|
|
|
+ final_name = name + "_over_" + param if name else f"{'|'.join(set(models))}_over_{param}"
|
|
|
|
|
|
plt.figure(figsize=(8, 6))
|
|
|
|
|
|
- dynamic_colors = get_dynamic_colors(results, bw)
|
|
|
- for label, (df, sim_id, model_type) in results.items():
|
|
|
- sim_style = SIMULATOR_TRAITS.get(sim_id, {})
|
|
|
- marker = sim_style.get("marker", "x")
|
|
|
- lstyle = sim_style.get("ls", "-")
|
|
|
- colour = dynamic_colors.get((model_type, sim_id), "#7f7f7f")
|
|
|
-
|
|
|
+ for label, df, style in results:
|
|
|
plt.plot(
|
|
|
df[param],
|
|
|
df["total_time"],
|
|
|
label=label,
|
|
|
- color=colour,
|
|
|
- linestyle=lstyle,
|
|
|
- marker=marker,
|
|
|
- markersize=4,
|
|
|
+ color=style["color"],
|
|
|
+ linestyle=style["ls"],
|
|
|
+ marker=style["marker"],
|
|
|
+ markersize=5,
|
|
|
markevery=3,
|
|
|
)
|
|
|
|
|
|
@@ -199,31 +143,26 @@ def plot_time_vs_param(csv_files, param, fixed_param=None, fixed_value=None, max
|
|
|
plt.legend()
|
|
|
plt.grid(True, linestyle=':', alpha=0.6)
|
|
|
|
|
|
- script_dir = Path(__file__).resolve().parent
|
|
|
- output_dir = script_dir / "Plots"
|
|
|
- if o_path:
|
|
|
- output_dir = output_dir / o_path
|
|
|
-
|
|
|
+ output_dir = Path(__file__).resolve().parent / "Plots" / o_path
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
output_file = output_dir / f"{final_name}.png"
|
|
|
+
|
|
|
plt.tight_layout()
|
|
|
plt.savefig(output_file, dpi=300)
|
|
|
print(f"Saved plot as {output_file}")
|
|
|
|
|
|
|
|
|
-def plot_time_vs_models(csv_files, o_path="", name="", bw=False):
|
|
|
- Atomics = "#Models"
|
|
|
- results = {}
|
|
|
+def plot_time_vs_models(csv_files, styles, o_path="", name=""):
|
|
|
+ Atomics = "Models"
|
|
|
+ results = []
|
|
|
models = []
|
|
|
+
|
|
|
for i, path in enumerate(csv_files):
|
|
|
df = pd.read_csv(path)
|
|
|
sim_id = df["simulator"].iloc[0] if "simulator" in df.columns else "None"
|
|
|
model_type = df["model_type"].iloc[0] if "model_type" in df.columns else "None"
|
|
|
models.append(model_type)
|
|
|
|
|
|
- if "model_type" in df.columns:
|
|
|
- models.append(path.stem)
|
|
|
-
|
|
|
df = clean_data(df)
|
|
|
df = df.copy()
|
|
|
df[Atomics] = df.apply(
|
|
|
@@ -237,55 +176,39 @@ def plot_time_vs_models(csv_files, o_path="", name="", bw=False):
|
|
|
df = df.sort_values(by=Atomics)
|
|
|
|
|
|
grouped = df.groupby(Atomics, as_index=False)["total_time"].mean()
|
|
|
+ label = f"{model_type} ({sim_id})"
|
|
|
|
|
|
- # Determine the label
|
|
|
- sim_label = SIMULATOR_TRAITS.get(sim_id, {}).get("label", sim_id)
|
|
|
- label = f"{model_type} ({sim_label})"
|
|
|
+ style_input = styles[i] if i < len(styles) else list(PREDEFINED_STYLES.keys())[i % 6]
|
|
|
+ style = get_style_config(style_input)
|
|
|
|
|
|
- results[label] = grouped, sim_id, model_type
|
|
|
+ results.append((label, grouped, style))
|
|
|
|
|
|
- if not name:
|
|
|
- unique_models = "|".join(set(models))
|
|
|
- final_name = f"{unique_models}_over_{Atomics}"
|
|
|
- else:
|
|
|
- final_name = name + "_" + Atomics
|
|
|
+ final_name = name + "_over_" + Atomics if name else f"{'|'.join(set(models))}_over_{Atomics}"
|
|
|
|
|
|
plt.figure(figsize=(8, 6))
|
|
|
|
|
|
- dynamic_colors = get_dynamic_colors(results, bw)
|
|
|
- for label, (df, sim_id, model_type) in results.items():
|
|
|
- sim_style = SIMULATOR_TRAITS.get(sim_id, {})
|
|
|
- marker = sim_style.get("marker", "x")
|
|
|
- colour = dynamic_colors.get((model_type, sim_id), "#7f7f7f")
|
|
|
-
|
|
|
+ for label, df, style in results:
|
|
|
plt.plot(
|
|
|
df[Atomics],
|
|
|
df["total_time"],
|
|
|
label=label,
|
|
|
- color=colour,
|
|
|
- marker=marker,
|
|
|
+ color=style["color"],
|
|
|
+ marker=style["marker"],
|
|
|
linestyle='',
|
|
|
- markersize=3,
|
|
|
- markeredgecolor='none',
|
|
|
- alpha=0.6,
|
|
|
+ markersize=4,
|
|
|
+ alpha=0.7,
|
|
|
)
|
|
|
|
|
|
- title = f"Average Simulation Time vs {Atomics}"
|
|
|
- plt.title(title)
|
|
|
+ plt.title(f"Average Simulation Time vs {Atomics}")
|
|
|
plt.xlabel(Atomics)
|
|
|
plt.ylabel("Average Simulation Time (s)")
|
|
|
plt.legend()
|
|
|
plt.grid(True, linestyle=':', alpha=0.6)
|
|
|
|
|
|
- # Set directory relative to script parent
|
|
|
- script_dir = Path(__file__).resolve().parent
|
|
|
- output_dir = script_dir / "Plots"
|
|
|
- if o_path:
|
|
|
- output_dir = output_dir / o_path
|
|
|
-
|
|
|
+ output_dir = Path(__file__).resolve().parent / "Plots" / o_path
|
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
-
|
|
|
output_file = output_dir / f"{final_name}.png"
|
|
|
+
|
|
|
plt.tight_layout()
|
|
|
plt.savefig(output_file, dpi=300)
|
|
|
print(f"Saved plot as {output_file}")
|
|
|
@@ -308,14 +231,26 @@ def main():
|
|
|
# Subcommand: total (Atomic Models)
|
|
|
parser_tot = subparsers.add_parser("total", help="Plot total time vs number of atomic models")
|
|
|
|
|
|
+ # Shared arguments for all subparsers
|
|
|
for subp in [parser_tvparam, parser_tvn, parser_tot]:
|
|
|
- subp.add_argument("--bw", action="store_true", help="Black and white / Colourblind friendly mode")
|
|
|
subp.add_argument("csv_files", nargs="+", type=Path, help="CSV files to process")
|
|
|
+ subp.add_argument(
|
|
|
+ "-s", "--styles",
|
|
|
+ type=str,
|
|
|
+ nargs="+",
|
|
|
+ help=f"Space-separated list of colors matching CSV order. Options: {', '.join(VALID_STYLE_INPUTS)}"
|
|
|
+ )
|
|
|
subp.add_argument("-p", "--path", type=str, default="", help="Subdirectory for storing plots")
|
|
|
subp.add_argument("-n", "--name", type=str, help="Name of output file")
|
|
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
+ # Default to cycling through styles if the user omits the -s flag
|
|
|
+ if not args.styles:
|
|
|
+ styles = list(PREDEFINED_STYLES.keys())[:len(args.csv_files)]
|
|
|
+ else:
|
|
|
+ styles = args.styles
|
|
|
+
|
|
|
if args.command == "param":
|
|
|
if args.width is not None:
|
|
|
fixed_param, fixed_value = "width", args.width
|
|
|
@@ -323,14 +258,17 @@ def main():
|
|
|
else:
|
|
|
fixed_param, fixed_value = "depth", args.depth
|
|
|
varying_param = "width"
|
|
|
- plot_time_vs_param(args.csv_files, param=varying_param,
|
|
|
+
|
|
|
+ plot_time_vs_param(args.csv_files, styles, param=varying_param,
|
|
|
fixed_param=fixed_param, fixed_value=fixed_value,
|
|
|
- max_value=args.max, o_path=args.path, name=args.name, bw=args.bw)
|
|
|
+ max_value=args.max, o_path=args.path, name=args.name)
|
|
|
+
|
|
|
elif args.command == "number":
|
|
|
- plot_time_vs_param(args.csv_files, param="number", o_path=args.path, name=args.name, bw=args.bw)
|
|
|
+ plot_time_vs_param(args.csv_files, styles, param="number", o_path=args.path, name=args.name)
|
|
|
+
|
|
|
elif args.command == "total":
|
|
|
- plot_time_vs_models(args.csv_files, o_path=args.path, name=args.name, bw=args.bw)
|
|
|
+ plot_time_vs_models(args.csv_files, styles, o_path=args.path, name=args.name)
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
- main()
|
|
|
+ main()
|