Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
224 changes: 87 additions & 137 deletions evaluation/state_metrics/EXPERIMENT_SCENARIOS.md

Large diffs are not rendered by default.

516 changes: 173 additions & 343 deletions evaluation/state_metrics/PAPER_SCENARIOS.md

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions evaluation/state_metrics/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,20 @@ class DatasetSpec:
remove_from_profile="Worker",
default_levels=(0, 1, 2, 3),
),
# Model-error routing synthetic (scenario #3): a chain of case_type-driven
# XOR splits with LENGTH-ASYMMETRIC branches (long A-branch chain vs short
# single-activity B-branch), separated by common activities. The base model
# routes CORRECTLY (red->long, blue->short); the `route_error` perturbation
# inverts the routing on the first `level` gateways. Generated by
# tools/generate_route_error.py. `remove_from_profile` points at its single
# pool so the resources/duration families still work for sanity baselines.
"synthetic_route_error": DatasetSpec(
name="synthetic_route_error",
bpmn=REPO_ROOT / "samples" / "dev-samples" / "synthetic_route_error.bpmn",
params=REPO_ROOT / "samples" / "dev-samples" / "synthetic_route_error.json",
remove_from_profile="Worker",
default_levels=(0, 1, 2, 3),
),
# Parallel-automation synthetic (scenario #2): an AND block with a long
# critical branch and a short non-critical chain on a SEPARATE pool.
# Generated by tools/generate_parallel_auto.py. The non-critical tasks
Expand Down
73 changes: 73 additions & 0 deletions evaluation/state_metrics/perturb.py
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,79 @@ def _is_split(entry: dict) -> bool:
}


def build_route_error_params(
base_json_path: str | Path,
*,
n_gateways_inverted: int,
out_json_path: str | Path,
) -> dict:
"""Invert the ``case_type`` routing on the first ``n_gateways_inverted``
XOR splits — the *model error* for scenario #3.

Designed for the *route-error* synthetic (see
``tools/generate_route_error.py``), whose base params already route
CORRECTLY by case_type at every split gateway: the ``_a`` (long) branch is
bound to ``rule_red`` and the ``_b`` (short) branch to ``rule_blue`` via
``condition_id``. This builder rewrites the first ``n_gateways_inverted``
split gateways (ordered by gateway id) to **swap** those bindings — the
``_a`` branch now fires on ``rule_blue`` and the ``_b`` branch on
``rule_red`` — so red cases take the short branch and blue cases the long
branch at those gateways.

Because the population is 50/50 red/blue, each gateway's path marginal stays
50/50, so cycle time, the 2-gram distribution and the aggregate
relative-event-distribution are all unchanged; only each case's *own* path
(paired by case_id) flips. ``n_gateways_inverted == 0`` is a no-op (copies
the base params verbatim).

Returns a manifest listing which gateways were inverted.
"""
if n_gateways_inverted < 0:
raise ValueError("n_gateways_inverted must be >= 0")
params = _load_params(base_json_path)

# A "split" gateway here is any branching entry with exactly two paths whose
# ids end in _a / _b — the convention emitted by the route-error generator.
def _is_split(entry: dict) -> bool:
ids = [p["path_id"] for p in entry.get("probabilities", [])]
return len(ids) == 2 and any(i.endswith("_a") for i in ids) and any(
i.endswith("_b") for i in ids
)

splits = sorted(
(e for e in params.get("gateway_branching_probabilities", []) if _is_split(e)),
key=lambda e: e["gateway_id"],
)
if n_gateways_inverted > len(splits):
raise ValueError(
f"requested {n_gateways_inverted} inverted gateways but only "
f"{len(splits)} split gateways exist"
)

inverted: list[str] = []
for entry in splits[:n_gateways_inverted]:
for p in entry["probabilities"]:
# Swap the rule so the long (_a) branch fires on blue and the short
# (_b) branch on red — the inverse of the correct base routing.
if "condition_id" not in p:
raise ValueError(
f"gateway {entry['gateway_id']!r} path {p['path_id']!r} has no "
"condition_id; base params must route by case_type "
"(generate_route_error bakes this in)"
)
p["condition_id"] = (
"rule_blue" if p["path_id"].endswith("_a") else "rule_red"
)
inverted.append(entry["gateway_id"])

_write_params(params, out_json_path)
return {
"n_gateways_inverted": n_gateways_inverted,
"inverted_gateways": inverted,
"n_split_gateways": len(splits),
}


def build_branch_automation_params(
base_json_path: str | Path,
*,
Expand Down
14 changes: 14 additions & 0 deletions evaluation/state_metrics/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
build_gateway_biased_params,
build_perturbed_params,
build_role_swap_params,
build_route_error_params,
)
from src.process_state_prosimos_run import run_basic_simulation
from src.runner import run_process_state_and_simulation
Expand Down Expand Up @@ -846,6 +847,19 @@ def _prepare_params_for_level(
shift=signed,
out_json_path=out_params,
)
elif cfg.perturbation == "route_error":
if level < 0:
raise ValueError(
"route_error levels must be >= 0 (number of gateways inverted)"
)
# level = number of leading XOR splits whose case_type routing is
# inverted (red->short, blue->long) — the model error. The base params
# already route CORRECTLY, so level 0 is a verbatim copy.
manifest = build_route_error_params(
cfg.params_path,
n_gateways_inverted=level,
out_json_path=out_params,
)
else:
raise ValueError(f"unknown perturbation type {cfg.perturbation!r}")
manifest = {"perturbation": cfg.perturbation, "level": level, **manifest}
Expand Down
5 changes: 4 additions & 1 deletion evaluation/state_metrics/run_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def main() -> None:
"arrival_burst", "relabel", "rephase",
"mix_ratio", "label_swap", "case_route",
"parallel_auto", "front_back_load",
"case_type_drift"],
"case_type_drift", "route_error"],
default="resources",
help="which perturbation family to apply")
p.add_argument("--remove-from-profile", default=None,
Expand Down Expand Up @@ -164,6 +164,9 @@ def main() -> None:
elif args.perturbation == "front_back_load":
# Percentage of duration mass moved toward the chosen chain end.
levels = spec.default_levels
elif args.perturbation == "route_error":
# Number of leading XOR splits whose case_type routing is inverted.
levels = spec.default_levels
else:
levels = spec.default_levels

Expand Down
1 change: 1 addition & 0 deletions samples/dev-samples/synthetic_route_error.bpmn
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" id="Definitions_route_error" targetNamespace="http://bpmn.io/schema/bpmn"><bpmn:process id="Process_route_error" isExecutable="false"><bpmn:startEvent id="StartEvent_1" name="Start"><bpmn:outgoing>f_start</bpmn:outgoing></bpmn:startEvent><bpmn:task id="t_reg" name="Register"><bpmn:incoming>f_start</bpmn:incoming><bpmn:outgoing>f_reg_g1s</bpmn:outgoing></bpmn:task><bpmn:exclusiveGateway id="g1s" name="XOR-split-1"><bpmn:incoming>f_reg_g1s</bpmn:incoming><bpmn:outgoing>f_g1s_a</bpmn:outgoing><bpmn:outgoing>f_g1s_b</bpmn:outgoing></bpmn:exclusiveGateway><bpmn:task id="t_a1_1" name="Assess_A1_1"><bpmn:incoming>f_g1s_a</bpmn:incoming><bpmn:outgoing>f_a1_link1</bpmn:outgoing></bpmn:task><bpmn:task id="t_a1_2" name="Assess_A1_2"><bpmn:incoming>f_a1_link1</bpmn:incoming><bpmn:outgoing>f_a1_link2</bpmn:outgoing></bpmn:task><bpmn:task id="t_a1_3" name="Assess_A1_3"><bpmn:incoming>f_a1_link2</bpmn:incoming><bpmn:outgoing>f_a1_g1j</bpmn:outgoing></bpmn:task><bpmn:task id="t_b1_1" name="Assess_B1_1"><bpmn:incoming>f_g1s_b</bpmn:incoming><bpmn:outgoing>f_b1_link1</bpmn:outgoing></bpmn:task><bpmn:task id="t_b1_2" name="Assess_B1_2"><bpmn:incoming>f_b1_link1</bpmn:incoming><bpmn:outgoing>f_b1_link2</bpmn:outgoing></bpmn:task><bpmn:task id="t_b1_3" name="Assess_B1_3"><bpmn:incoming>f_b1_link2</bpmn:incoming><bpmn:outgoing>f_b1_g1j</bpmn:outgoing></bpmn:task><bpmn:exclusiveGateway id="g1j" name="XOR-join-1"><bpmn:incoming>f_a1_g1j</bpmn:incoming><bpmn:incoming>f_b1_g1j</bpmn:incoming><bpmn:outgoing>f_g1j_t_tri1</bpmn:outgoing></bpmn:exclusiveGateway><bpmn:task id="t_tri1" name="Triage_1"><bpmn:incoming>f_g1j_t_tri1</bpmn:incoming><bpmn:outgoing>f_t_tri1_g2s</bpmn:outgoing></bpmn:task><bpmn:exclusiveGateway id="g2s" name="XOR-split-2"><bpmn:incoming>f_t_tri1_g2s</bpmn:incoming><bpmn:outgoing>f_g2s_a</bpmn:outgoing><bpmn:outgoing>f_g2s_b</bpmn:outgoing></bpmn:exclusiveGateway><bpmn:task id="t_a2_1" name="Assess_A2_1"><bpmn:incoming>f_g2s_a</bpmn:incoming><bpmn:outgoing>f_a2_link1</bpmn:outgoing></bpmn:task><bpmn:task id="t_a2_2" name="Assess_A2_2"><bpmn:incoming>f_a2_link1</bpmn:incoming><bpmn:outgoing>f_a2_link2</bpmn:outgoing></bpmn:task><bpmn:task id="t_a2_3" name="Assess_A2_3"><bpmn:incoming>f_a2_link2</bpmn:incoming><bpmn:outgoing>f_a2_g2j</bpmn:outgoing></bpmn:task><bpmn:task id="t_b2_1" name="Assess_B2_1"><bpmn:incoming>f_g2s_b</bpmn:incoming><bpmn:outgoing>f_b2_link1</bpmn:outgoing></bpmn:task><bpmn:task id="t_b2_2" name="Assess_B2_2"><bpmn:incoming>f_b2_link1</bpmn:incoming><bpmn:outgoing>f_b2_link2</bpmn:outgoing></bpmn:task><bpmn:task id="t_b2_3" name="Assess_B2_3"><bpmn:incoming>f_b2_link2</bpmn:incoming><bpmn:outgoing>f_b2_g2j</bpmn:outgoing></bpmn:task><bpmn:exclusiveGateway id="g2j" name="XOR-join-2"><bpmn:incoming>f_a2_g2j</bpmn:incoming><bpmn:incoming>f_b2_g2j</bpmn:incoming><bpmn:outgoing>f_g2j_t_tri2</bpmn:outgoing></bpmn:exclusiveGateway><bpmn:task id="t_tri2" name="Triage_2"><bpmn:incoming>f_g2j_t_tri2</bpmn:incoming><bpmn:outgoing>f_t_tri2_g3s</bpmn:outgoing></bpmn:task><bpmn:exclusiveGateway id="g3s" name="XOR-split-3"><bpmn:incoming>f_t_tri2_g3s</bpmn:incoming><bpmn:outgoing>f_g3s_a</bpmn:outgoing><bpmn:outgoing>f_g3s_b</bpmn:outgoing></bpmn:exclusiveGateway><bpmn:task id="t_a3_1" name="Assess_A3_1"><bpmn:incoming>f_g3s_a</bpmn:incoming><bpmn:outgoing>f_a3_link1</bpmn:outgoing></bpmn:task><bpmn:task id="t_a3_2" name="Assess_A3_2"><bpmn:incoming>f_a3_link1</bpmn:incoming><bpmn:outgoing>f_a3_link2</bpmn:outgoing></bpmn:task><bpmn:task id="t_a3_3" name="Assess_A3_3"><bpmn:incoming>f_a3_link2</bpmn:incoming><bpmn:outgoing>f_a3_g3j</bpmn:outgoing></bpmn:task><bpmn:task id="t_b3_1" name="Assess_B3_1"><bpmn:incoming>f_g3s_b</bpmn:incoming><bpmn:outgoing>f_b3_link1</bpmn:outgoing></bpmn:task><bpmn:task id="t_b3_2" name="Assess_B3_2"><bpmn:incoming>f_b3_link1</bpmn:incoming><bpmn:outgoing>f_b3_link2</bpmn:outgoing></bpmn:task><bpmn:task id="t_b3_3" name="Assess_B3_3"><bpmn:incoming>f_b3_link2</bpmn:incoming><bpmn:outgoing>f_b3_g3j</bpmn:outgoing></bpmn:task><bpmn:exclusiveGateway id="g3j" name="XOR-join-3"><bpmn:incoming>f_a3_g3j</bpmn:incoming><bpmn:incoming>f_b3_g3j</bpmn:incoming><bpmn:outgoing>f_g3j_t_dec</bpmn:outgoing></bpmn:exclusiveGateway><bpmn:task id="t_dec" name="Decide"><bpmn:incoming>f_g3j_t_dec</bpmn:incoming><bpmn:outgoing>f_dec_end</bpmn:outgoing></bpmn:task><bpmn:endEvent id="EndEvent_1" name="End"><bpmn:incoming>f_dec_end</bpmn:incoming></bpmn:endEvent><bpmn:sequenceFlow id="f_start" sourceRef="StartEvent_1" targetRef="t_reg" /><bpmn:sequenceFlow id="f_reg_g1s" sourceRef="t_reg" targetRef="g1s" /><bpmn:sequenceFlow id="f_g1s_a" sourceRef="g1s" targetRef="t_a1_1" /><bpmn:sequenceFlow id="f_a1_link1" sourceRef="t_a1_1" targetRef="t_a1_2" /><bpmn:sequenceFlow id="f_a1_link2" sourceRef="t_a1_2" targetRef="t_a1_3" /><bpmn:sequenceFlow id="f_a1_g1j" sourceRef="t_a1_3" targetRef="g1j" /><bpmn:sequenceFlow id="f_g1s_b" sourceRef="g1s" targetRef="t_b1_1" /><bpmn:sequenceFlow id="f_b1_link1" sourceRef="t_b1_1" targetRef="t_b1_2" /><bpmn:sequenceFlow id="f_b1_link2" sourceRef="t_b1_2" targetRef="t_b1_3" /><bpmn:sequenceFlow id="f_b1_g1j" sourceRef="t_b1_3" targetRef="g1j" /><bpmn:sequenceFlow id="f_g1j_t_tri1" sourceRef="g1j" targetRef="t_tri1" /><bpmn:sequenceFlow id="f_t_tri1_g2s" sourceRef="t_tri1" targetRef="g2s" /><bpmn:sequenceFlow id="f_g2s_a" sourceRef="g2s" targetRef="t_a2_1" /><bpmn:sequenceFlow id="f_a2_link1" sourceRef="t_a2_1" targetRef="t_a2_2" /><bpmn:sequenceFlow id="f_a2_link2" sourceRef="t_a2_2" targetRef="t_a2_3" /><bpmn:sequenceFlow id="f_a2_g2j" sourceRef="t_a2_3" targetRef="g2j" /><bpmn:sequenceFlow id="f_g2s_b" sourceRef="g2s" targetRef="t_b2_1" /><bpmn:sequenceFlow id="f_b2_link1" sourceRef="t_b2_1" targetRef="t_b2_2" /><bpmn:sequenceFlow id="f_b2_link2" sourceRef="t_b2_2" targetRef="t_b2_3" /><bpmn:sequenceFlow id="f_b2_g2j" sourceRef="t_b2_3" targetRef="g2j" /><bpmn:sequenceFlow id="f_g2j_t_tri2" sourceRef="g2j" targetRef="t_tri2" /><bpmn:sequenceFlow id="f_t_tri2_g3s" sourceRef="t_tri2" targetRef="g3s" /><bpmn:sequenceFlow id="f_g3s_a" sourceRef="g3s" targetRef="t_a3_1" /><bpmn:sequenceFlow id="f_a3_link1" sourceRef="t_a3_1" targetRef="t_a3_2" /><bpmn:sequenceFlow id="f_a3_link2" sourceRef="t_a3_2" targetRef="t_a3_3" /><bpmn:sequenceFlow id="f_a3_g3j" sourceRef="t_a3_3" targetRef="g3j" /><bpmn:sequenceFlow id="f_g3s_b" sourceRef="g3s" targetRef="t_b3_1" /><bpmn:sequenceFlow id="f_b3_link1" sourceRef="t_b3_1" targetRef="t_b3_2" /><bpmn:sequenceFlow id="f_b3_link2" sourceRef="t_b3_2" targetRef="t_b3_3" /><bpmn:sequenceFlow id="f_b3_g3j" sourceRef="t_b3_3" targetRef="g3j" /><bpmn:sequenceFlow id="f_g3j_t_dec" sourceRef="g3j" targetRef="t_dec" /><bpmn:sequenceFlow id="f_dec_end" sourceRef="t_dec" targetRef="EndEvent_1" /></bpmn:process></bpmn:definitions>
Loading