diff --git a/evaluation/state_metrics/EXPERIMENT_SCENARIOS.md b/evaluation/state_metrics/EXPERIMENT_SCENARIOS.md new file mode 100644 index 0000000..79df6ce --- /dev/null +++ b/evaluation/state_metrics/EXPERIMENT_SCENARIOS.md @@ -0,0 +1,156 @@ +# State-metrics experiment scenarios — table + descriptions + +Standalone reference for the short-term-simulation state-metrics paper: the +candidate experiment table and a plain-English description of every scenario. +For design rationale, build plans, and implementation status see the companion +`PAPER_SCENARIOS.md`. + +**Goal:** show that the proposed per-instant state metric **replicates what the +baselines detect and also covers cases they miss** (complementary, not +adversarial). + +**Baselines referenced:** cycle-time MAE (`cycle_time`), N-gram distance +(`ngd_n2`), relative event distribution (`red`), remaining-time distribution +(`rtd`). + +**Design principle:** hold every *aggregate / marginal* summary fixed (cycle +time, resource utilization ρ, WIP/λ, overall ratios, activity counts) so the +baselines are blind **by construction**, and change only *what is active*, +*where in time*, or *which type is paired with which path* — which is exactly +what the per-instant state metric measures. + +**Three "win" mechanisms:** +1. **Composition** — *what* is active changes (activity mix / labels) while + aggregate timing is untouched. +2. **Timeline localization** — *where / when* the error occurs, even when the + aggregate cycle-time distribution is unchanged. +3. **Pairing** — case-type-aware: a shift visible only when comparing *paired* + sub-populations, invisible to any marginal distribution. + +## Table + +`Attr?`: **native** = real simulator case attribute · **labels** = post-hoc +red/green tags · **no** = none. c-index: 1.0 = perfect monotone detection, +0.5 = blind. + +| # | Scenario | Attr? | Invariants held | Mechanism | Winner / blind baselines | Family | Evidence | +|---|----------|-------|-----------------|-----------|--------------------------|--------|----------| +| 1 | **Single-activity rename** (pure composition) | no | cycle time, ρ, WIP, *timeline identical* | composition | **State** (`activity`, `activity_case`) wins; `cycle_time`/`ngd`/`red`/`rtd` blind | ✅ `relabel` | **Strong** — cycle_time exactly 0; activity c-index up to 1.0 | +| 2 | **Parallel-branch automation** (automate non-critical path; critical path sets cycle time) | no | cycle time, ρ, WIP | composition / timeline | **State** wins; `cycle_time` blind | ✅ `parallel_auto` (sim-driven) | **Strong** — state (activity/case/cardinality/role) c-index 1.0; ngd/red/cycle_time/rtd all 0.5 | +| 3 | **XOR redistribution**, equal-duration branches (shift gateway probs / mix ratio) | labels | mean cycle time, ρ, WIP | composition | mixed — **ngd competitive / wins** | ⚠️ `mix_ratio`, `gateway` | **Cautionary** — ngd ~0.97 most monotonic, state ~0.84 (state *loses*) | +| 4 | **Front-load vs back-load** durations (+ swap resource capacity to keep ρ symmetric) | no | aggregate cycle time, ρ, WIP | **timeline / "where"** | **State (time-weighted)** wins; `cycle_time`, `ngd` blind | ✅ `front_back_load` (sim-driven) | **Strong** — state c-index 1.0; cycle_time/ngd/red/rtd all 0.5 (per-case total held exactly invariant) | +| 5 | **Asymmetric case-type** (red/blue), distribution unchanged, pairing reveals it | **native** | cycle time **distribution**, ρ, WIP | **pairing** | **State** (`case_type`, `activity_type`) wins; **all** baselines ~0.5 (blind) | ✅ `case_route` (sim-driven) + `label_swap` (post-hoc) | **Strongest** — activity_type c-index ≈ 0.92–0.94; every baseline 0.5 | +| 6 | **Temporal case-type drift** (case-type mix drifts along the arrival timeline) | **native** | cycle time, ρ, WIP, **global marginal** | **pairing + timeline** | **State** (`case_type` AND `activity_type`); all baselines blind | ✅ `case_type_drift` (sim-driven, synthetic controlled form) | **Strong** — `case_type`/`activity_type` c-index 1.0; every baseline AND type-agnostic state projection 0.5 | +| 6r | *(open)* **Real-life** temporal case-type shift (same mechanism, real log) | **native** | — (observational) | **pairing** | **State** (`case_type`/`activity_type`); risk: Jaccard ~0.9 floor on real logs | ❌ not built | data-discovery task; `case_type_drift` is its controlled synthetic stand-in. Optional "icing" | +| 7 | **Activity time-shift** (~30 min start/end shift) | no | composition, ρ, WIP | timeline | **`cycle_time` wins**; `ngd`/`red` blind — *state has no edge* | ✅ `calendar_shift` | **Cautionary** — cycle_time detects; dead-end for a state-advantage claim | +| — | *(bonus)* **Per-case timeline jitter** | no | per-case duration, gaps, composition | timeline | **State** wins; `ctd`/`ngd`/`red` exactly 0 | ✅ `rephase` | **Strong** — state c-index 0.94–1.0 | + +**Status summary:** +- ✅ **Ready state-wins:** #1, #2, #4, #5, #6, bonus +- ⚠️ **Cautionary** (baseline wins / no edge — keep only for the "complementary" framing): #3, #7 +- ❌ **Not built** (optional): #6r (real-log) + +## Scenario descriptions + +### 1 · Single-activity rename — `relabel` +- **Process:** any synthetic log. +- **Change:** rename a fraction of one activity's occurrences to a new label + (e.g. "Review" → "Review_alt"); nothing else moves. +- **Kept identical:** every timestamp, case, resource, duration — the *timing* + is byte-for-byte the same, only some event *names* change. +- **Why baselines miss it:** cycle_time / red / rtd only look at timing → zero + difference. +- **Why state catches it:** the set of *what's active* at each moment now holds + "Review_alt" instead of "Review" — a pure composition shift. *(Simplest + composition proof.)* + +### 2 · Parallel-branch automation — `parallel_auto` +- **Process:** an AND-split runs two things at once — a long **Critical** task + (10 min) and a short **Non-critical** chain (3×2 min) on a *separate* team — + then they join. The critical task alone sets how long the case takes. +- **Change:** "automate" the non-critical branch — shrink its durations toward + ~0 (a robot now does that paperwork instantly). +- **Kept identical:** total cycle time (critical path unmoved), and the + non-critical activity still *appears* once per case (sequence/bigrams + unchanged). +- **Why baselines miss it:** cycle_time unchanged; ngd sees the same activity + sequence. +- **Why state catches it:** that non-critical task used to *occupy* the active + set for 6 min/case and now occupies ~0 → at any instant less is genuinely + happening in parallel. *(Classic BPR "automate the side-process" example.)* + +### 3 · XOR redistribution — `mix_ratio` / `gateway` ⚠️ +- **Process:** a case takes one of two equally-long branches at a fork (or a + population mixes two case types). +- **Change:** shift the proportions — e.g. 70% take branch A instead of 50%. +- **Kept identical:** mean cycle time (branches equal-duration), ρ, WIP. +- **Result (cautionary):** the baseline **`ngd` actually wins** (~0.97 vs state + ~0.84), because changing branch proportions changes the activity-sequence + (bigram) histogram — exactly what ngd detects. Keep only as an honest + "baselines also do well sometimes" point; **not** a state win. + +### 4 · Front-load vs back-load — `front_back_load` +- **Process:** a simple 5-step sequence (Step 1→…→Step 5), all steps equal + length. +- **Change:** redistribute the *time* across steps — front-loading makes early + steps long and late steps short (back-loading is the mirror) — **while + keeping each case's total duration exactly the same.** +- **Kept identical:** total cycle time (held to the microsecond by + renormalization), ρ, the activity sequence. +- **Why baselines miss it:** cycle_time and ngd see "same total, same steps." +- **Why state catches it:** *where in time the work sits* changed — early there's + now more concurrent work, later less. The per-instant state view is the only + metric that localizes it. *(The "where is the error" / timeline story — most + novel.)* + +### 5 · Asymmetric case-type — `case_route` / `label_swap` *(strongest)* +- **Process:** cases carry a real red/blue attribute that **drives routing** + (red → branch A, blue → branch B), but both branches are equally long. +- **Change:** swap the red/blue tags on a fraction of cases (so some "red" cases + now sit on the B-branch, etc.). +- **Kept identical:** the red/blue *overall ratio* (50/50), all timing, activity + counts — every single-variable summary is unchanged. +- **Why baselines (and even simple state views) miss it:** no marginal moved — + red still 50%, branch-A still 50%, durations identical. The change is *only in + the pairing*: which type is on which branch. +- **Why state catches it:** the joint `(activity, case_type)` view sees + "branch-A is now run by blue cases" — the only metric comparing *paired* + sub-populations. *(All baselines 0.5; state ≈0.93. The unique contribution.)* + +### 6 · Temporal case-type drift — `case_type_drift` *(the data-attribute one)* +- **Process:** same red/blue attribute-routed process as #5. +- **Change:** instead of random swaps, make the red/blue mix **drift over time** + (early arrivals skew blue, later ones skew red) — **while keeping the overall + 50/50 ratio exactly fixed.** +- **Kept identical:** global red/blue ratio, all timing, activity counts. +- **Why baselines miss it:** overall composition is still 50/50 and the event + stream is untouched. +- **Why state catches it — and how it differs from #5:** because the mix varies + *along the timeline*, at any window more of one color is active than baseline → + **both** the simple `case_type` view *and* the paired `activity_type` view fire + (in #5 only `activity_type` fired). That extra `case_type` signal is the + fingerprint of *temporal* drift. + +### 6r · Real-life temporal case-type shift *(open / optional)* +- Same mechanism as #6, but on a **real** log whose case-type mix genuinely + drifts over months. Observational (no controlled invariant), gated on finding + such a log and on Jaccard not hitting its ~0.9 real-log floor. `case_type_drift` + is its controlled synthetic stand-in; left as optional data-discovery. + +### 7 · Activity time-shift — `calendar_shift` ⚠️ +- **Process:** a business-hours process. +- **Change:** shift one team's working hours by ~30 min–N hours. +- **Result (cautionary):** this is *not* cycle-time-neutral — shifting hours + desynchronizes hand-offs and **changes cycle time**, so `cycle_time` detects it + and **state has no special edge**. Kept only to illustrate a baseline's + strength. + +### — · Per-case timeline jitter *(bonus)* — `rephase` +- **Process:** any synthetic. +- **Change:** slide each case's *entire* timeline earlier/later by a small random + offset — the case is internally unchanged (same durations, gaps, order). +- **Kept identical (exactly zero by construction):** per-case cycle time, the + activity sequence, relative event positions → ctd / ngd / red are *exactly* 0. +- **Why state catches it:** even with each case internally untouched, *across* + cases the overlap pattern changes — at each instant a different set of cases is + active. Only the per-instant state view sees it. (state c-index 0.94–1.0) diff --git a/evaluation/state_metrics/PAPER_SCENARIOS.md b/evaluation/state_metrics/PAPER_SCENARIOS.md new file mode 100644 index 0000000..670cccc --- /dev/null +++ b/evaluation/state_metrics/PAPER_SCENARIOS.md @@ -0,0 +1,371 @@ +# Paper experiment scenarios — where state metrics win + +Candidate experiments for the short-term-simulation state-metrics paper, drawn +from the project meeting notes and cross-referenced with what is currently +implemented in this repo. The goal is to show, on controlled synthetic data, +that the proposed state-based metric **replicates what baselines detect and also +covers cases they miss** (complementary, not adversarial). + +Baselines referenced: cycle-time MAE (`cycle_time`), N-gram distance (`ngd_n2`), +relative event distribution (`red`), remaining-time distribution (`rtd`). + +## Design principle (from the meetings) + +> Hold **cycle time**, **resource utilization (ρ)**, and **WIP/λ** constant, so +> the baselines see "no difference" while the state metric does. + +Every scenario below is constructed to keep one or more of these invariants +fixed, isolating the effect that only the state metric is meant to catch. + +## The three "win" mechanisms + +The metric's advantage reduces to one of three mechanisms; a good paper set +covers all three with non-redundant examples: + +1. **Composition** — *what* is active changes (activity mix / labels) while + aggregate timing is untouched. +2. **Timeline localization** — *where / when* the error occurs, even when the + aggregate cycle-time distribution is unchanged. +3. **Pairing** — case-type-aware: a shift visible only when comparing *paired* + sub-populations, invisible to any marginal distribution. + +## Candidate experiments + +`Attr?` = does the scenario use case attributes? **native** = real simulator +case attribute; **labels** = artificial post-hoc red/green tags; **no** = none. + +| # | Scenario | Attr? | Invariants held | Mechanism | Expected winner / blind baselines | Implemented? | Evidence so far | +|---|----------|-------|-----------------|-----------|-----------------------------------|--------------|-----------------| +| 1 | **Single-activity rename** (pure composition) | no | cycle time, ρ, WIP, *timeline identical* | composition | **State** (`activity`, `activity_case`) wins; `cycle_time`/`ngd`/`red`/`rtd` blind | ✅ `relabel` | **Strong** — cycle_time exactly 0; activity c-index up to 1.0 | +| 2 | **Parallel-branch automation** (automate non-critical path; critical path sets cycle time) | no | cycle time, ρ, WIP | composition / timeline | **State** wins; `cycle_time` blind | ✅ `parallel_auto` (sim-driven) | **Strong** — state (activity/case/cardinality/role) c-index 1.0; ngd/red/cycle_time/rtd all 0.5 | +| 3 | **XOR redistribution**, equal-duration branches (shift gateway probs / mix ratio) | labels | mean cycle time, ρ, WIP | composition | mixed — **ngd competitive / wins** | ⚠️ `mix_ratio`, `gateway` | **Cautionary** — ngd ~0.97 most monotonic, state ~0.84 (state *loses*) | +| 4 | **Front-load vs back-load** durations (+ swap resource capacity to keep ρ symmetric) | no | aggregate cycle time, ρ, WIP | **timeline / "where"** | **State (time-weighted)** wins; `cycle_time`, `ngd` blind | ✅ `front_back_load` (sim-driven) | **Strong** — state c-index 1.0; cycle_time/ngd/red/rtd all 0.5 (per-case total held exactly invariant) | +| 5 | **Asymmetric case-type** (red/blue), distribution unchanged, pairing reveals it | **native** | cycle time **distribution**, ρ, WIP | **pairing** | **State** (`case_type`, `activity_type`) wins; **all** baselines ~0.5 (blind) | ✅ `case_route` (sim-driven) + `label_swap` (post-hoc) | **Strongest** — activity_type c-index ≈ 0.92–0.94; every baseline 0.5 | +| 6 | **Temporal case-type drift** (case-type mix drifts along the arrival timeline) | **native** | cycle time, ρ, WIP, **global marginal** | **pairing + timeline** | **State** (`case_type` AND `activity_type`); all baselines blind | ✅ `case_type_drift` (sim-driven, synthetic controlled form) | **Strong** — `case_type`/`activity_type` c-index 1.0; every baseline AND type-agnostic state projection 0.5 | +| 6r | *(open)* **Real-life** temporal case-type shift (same mechanism, real log) | **native** | — (observational) | **pairing** | **State** (`case_type`/`activity_type`); risk: Jaccard ~0.9 floor on real logs | ❌ not built | data-discovery task; `case_type_drift` is its controlled synthetic stand-in. Optional "icing" | +| 7 | **Activity time-shift** (~30 min start/end shift) | no | composition, ρ, WIP | timeline | **`cycle_time` wins**; `ngd`/`red` blind — *state has no edge* | ✅ `calendar_shift` | **Cautionary** — cycle_time detects; dead-end for a state-advantage claim | +| — | *(bonus)* **Per-case timeline jitter** | no | per-case duration, gaps, composition | timeline | **State** wins; `ctd`/`ngd`/`red` exactly 0 | ✅ `rephase` | **Strong** — state c-index 0.94–1.0 | + +## Scenarios explained (plain English) + +Each scenario is built the same way: hold every *aggregate / marginal* summary +fixed (cycle time, utilization, overall ratios, activity counts) so the baselines +are blind **by construction**, and change only *what is active*, *where in time*, +or *which type is paired with which path* — which is exactly what the per-instant +state metric measures. + +### 1 · Single-activity rename — `relabel` +- **Process:** any synthetic log. +- **Change:** rename a fraction of one activity's occurrences to a new label + (e.g. "Review" → "Review_alt"); nothing else moves. +- **Kept identical:** every timestamp, case, resource, duration — the *timing* + is byte-for-byte the same, only some event *names* change. +- **Why baselines miss it:** cycle_time / red / rtd only look at timing → zero + difference. +- **Why state catches it:** the set of *what's active* at each moment now holds + "Review_alt" instead of "Review" — a pure composition shift. *(Simplest + composition proof.)* + +### 2 · Parallel-branch automation — `parallel_auto` +- **Process:** an AND-split runs two things at once — a long **Critical** task + (10 min) and a short **Non-critical** chain (3×2 min) on a *separate* team — + then they join. The critical task alone sets how long the case takes. +- **Change:** "automate" the non-critical branch — shrink its durations toward + ~0 (a robot now does that paperwork instantly). +- **Kept identical:** total cycle time (critical path unmoved), and the + non-critical activity still *appears* once per case (sequence/bigrams + unchanged). +- **Why baselines miss it:** cycle_time unchanged; ngd sees the same activity + sequence. +- **Why state catches it:** that non-critical task used to *occupy* the active + set for 6 min/case and now occupies ~0 → at any instant less is genuinely + happening in parallel. *(Classic BPR "automate the side-process" example.)* + +### 3 · XOR redistribution — `mix_ratio` / `gateway` ⚠️ +- **Process:** a case takes one of two equally-long branches at a fork (or a + population mixes two case types). +- **Change:** shift the proportions — e.g. 70% take branch A instead of 50%. +- **Kept identical:** mean cycle time (branches equal-duration), ρ, WIP. +- **Result (cautionary):** the baseline **`ngd` actually wins** (~0.97 vs state + ~0.84), because changing branch proportions changes the activity-sequence + (bigram) histogram — exactly what ngd detects. Keep only as an honest + "baselines also do well sometimes" point; **not** a state win. + +### 4 · Front-load vs back-load — `front_back_load` +- **Process:** a simple 5-step sequence (Step 1→…→Step 5), all steps equal + length. +- **Change:** redistribute the *time* across steps — front-loading makes early + steps long and late steps short (back-loading is the mirror) — **while + keeping each case's total duration exactly the same.** +- **Kept identical:** total cycle time (held to the microsecond by + renormalization), ρ, the activity sequence. +- **Why baselines miss it:** cycle_time and ngd see "same total, same steps." +- **Why state catches it:** *where in time the work sits* changed — early there's + now more concurrent work, later less. The per-instant state view is the only + metric that localizes it. *(The "where is the error" / timeline story — most + novel.)* + +### 5 · Asymmetric case-type — `case_route` / `label_swap` *(strongest)* +- **Process:** cases carry a real red/blue attribute that **drives routing** + (red → branch A, blue → branch B), but both branches are equally long. +- **Change:** swap the red/blue tags on a fraction of cases (so some "red" cases + now sit on the B-branch, etc.). +- **Kept identical:** the red/blue *overall ratio* (50/50), all timing, activity + counts — every single-variable summary is unchanged. +- **Why baselines (and even simple state views) miss it:** no marginal moved — + red still 50%, branch-A still 50%, durations identical. The change is *only in + the pairing*: which type is on which branch. +- **Why state catches it:** the joint `(activity, case_type)` view sees + "branch-A is now run by blue cases" — the only metric comparing *paired* + sub-populations. *(All baselines 0.5; state ≈0.93. The unique contribution.)* + +### 6 · Temporal case-type drift — `case_type_drift` *(the data-attribute one)* +- **Process:** same red/blue attribute-routed process as #5. +- **Change:** instead of random swaps, make the red/blue mix **drift over time** + (early arrivals skew blue, later ones skew red) — **while keeping the overall + 50/50 ratio exactly fixed.** +- **Kept identical:** global red/blue ratio, all timing, activity counts. +- **Why baselines miss it:** overall composition is still 50/50 and the event + stream is untouched. +- **Why state catches it — and how it differs from #5:** because the mix varies + *along the timeline*, at any window more of one color is active than baseline → + **both** the simple `case_type` view *and* the paired `activity_type` view fire + (in #5 only `activity_type` fired). That extra `case_type` signal is the + fingerprint of *temporal* drift. + +### 6r · Real-life temporal case-type shift *(open / optional)* +- Same mechanism as #6, but on a **real** log whose case-type mix genuinely + drifts over months. Observational (no controlled invariant), gated on finding + such a log and on Jaccard not hitting its ~0.9 real-log floor. `case_type_drift` + is its controlled synthetic stand-in; left as optional data-discovery. + +### 7 · Activity time-shift — `calendar_shift` ⚠️ +- **Process:** a business-hours process. +- **Change:** shift one team's working hours by ~30 min–N hours. +- **Result (cautionary):** this is *not* cycle-time-neutral — shifting hours + desynchronizes hand-offs and **changes cycle time**, so `cycle_time` detects it + and **state has no special edge**. Kept only to illustrate a baseline's + strength. + +### — · Per-case timeline jitter *(bonus)* — `rephase` +- **Process:** any synthetic. +- **Change:** slide each case's *entire* timeline earlier/later by a small random + offset — the case is internally unchanged (same durations, gaps, order). +- **Kept identical (exactly zero by construction):** per-case cycle time, the + activity sequence, relative event positions → ctd / ngd / red are *exactly* 0. +- **Why state catches it:** even with each case internally untouched, *across* + cases the overlap pattern changes — at each instant a different set of cases is + active. Only the per-instant state view sees it. (state c-index 0.94–1.0) + +## Attribute-related subset + +- **#5 Asymmetric case-type** — the core, using **native** simulator case + attributes. Pairing pillar; strongest evidence. +- **#6 Temporal case-type drift** (`case_type_drift`) — **built**. A controlled + synthetic drift of the native `case_type` over the arrival timeline (global + marginal held fixed) on a genuinely attribute-routed short-term sim. Detected + by **both** `case_type` and `activity_type` (c-index 1.0) — the extra + `case_type` hit over `label_swap` is the temporal-drift signature. The + real-log form (#6r) remains optional data-discovery; this is its stand-in. +- **#3 XOR redistribution (`mix_ratio`)** — only *attribute-adjacent*: it tags + cases red/green with **artificial post-hoc labels**, not native attributes. + +Everything else (#1, #2, #4, #7, bonus) is attribute-free. + +## Cautionary / negative cases (baselines win — keep for the "complementary" framing only) + +- **#3** — `ngd` is most monotonic; do not feature it as a state win. +- **#7** — `cycle_time` detects the shift; state has no edge. Useful only to + show a baseline's strength, not ours. + +## Reading the table + +**Two cleanest pillars — already implemented and validated:** + +- **Composition pillar** → #1 `relabel` (baselines literally blind). +- **Pairing pillar** → #5 `case_route` / `label_swap` (the headline: all + baselines at 0.5, state ~0.94). The unique contribution to protect. + +**Avoid leading with #3** — our own runs show `ngd` *beats* state there, so it is +a weak/negative case to feature. Keep it only as an honest "baselines also do +well here" point supporting the complementary framing. + +**Two highest-value gaps — now built and validated** — each cycle-time-neutral, +demonstrating a *different* mechanism than the pillars: + +- **#4 front/back-loading** (`front_back_load`) — the candidate showcasing + **timeline localization** ("where the error is"). No baseline captures it + (all 0.5); state c-index 1.0. Per-case total duration held exactly invariant. +- **#2 parallel-branch automation** (`parallel_auto`) — a structural composition + change baselines can't see; reviewer-friendly (maps to BPR literature, e.g. + the Ford A/P redesign cited in the notes). State c-index 1.0, baselines 0.5. + +## Suggested demonstration set + +A tight, non-redundant set covering all three mechanisms: + +- **#1** (composition) + **#4** (timeline) + **#5** (pairing) — the core. +- **#2** as a second, *structural* (not just labeling) composition case. +- **#3** only as an honest "baselines also do well here" data point. + +## Implementation status note + +A constraint discovered while implementing #5: Prosimos can attribute-condition +**routing** but **not durations**. So the realizable form of "asymmetric +case-type" is duration-symmetric attribute-driven *routing* (`case_route`), not +the red-slower / blue-faster duration design from the notes — but the scenario's +*purpose* (pairing reveals what aggregates hide) is unchanged. + +See `evaluation/state_metrics/PERTURBATIONS.md` for the full perturbation +catalogue and `tests/e2e/` for the end-to-end attribute/routing verification. + +--- + +# Build plans — scenarios #2, #4, #6 ✅ DELIVERED + +> **Status (2026-06-28): all three are implemented, unit-tested, and validated +> end-to-end with real Prosimos.** Smoke runs (2 replicates, p90_wip cutoff) +> reproduce the intended result in every case — state metrics rank the +> perturbation monotonically (c-index 1.0) while the baselines stay at 0.5: +> +> | Scenario | Family | Dataset | State (c-index 1.0) | Baselines | +> |---|---|---|---|---| +> | #2 | `parallel_auto` | `synthetic_parallel_auto` | activity, case, cardinality, activity_role | ngd/red/cycle_time/rtd = 0.5 | +> | #4 | `front_back_load` | `synthetic_linear_chain` | all projections | ngd/red/cycle_time/rtd = 0.5 | +> | #6 | `case_type_drift` | `synthetic_case_route` | case_type, activity_type | ngd/red/cycle_time/rtd + type-agnostic state = 0.5 | +> +> New assets: generators `tools/generate_parallel_auto.py` and +> `tools/generate_linear_chain.py`; builders `build_branch_automation_params` / +> `build_front_back_load_params` in `perturb.py`; the `drift_case_types` +> transform + `_run_case_type_drift_levels` oracle in `pipeline.py`; dataset +> specs + CLI choices (`--perturbation parallel_auto|front_back_load|case_type_drift`, +> `--load-direction`). Unit tests in `tests/evaluation/state_metrics/`. +> +> **JSD/Jaccard:** explicitly *not* pursued (per decision 2026-06-28 — we keep +> Jaccard). The scenarios above are designed so the existing Jaccard + +> cardinality distances already separate cleanly on synthetic data. + +The original build notes are kept below for design provenance. + +How the machinery works (so the plans below slot in): + +- **Simulator-driven param families** (`resources`, `duration`, `gateway`, …): + add a `build__params(base, *, level…, out)` to `perturb.py`, then a + dispatch branch in `pipeline._prepare_params_for_level` (the + `if cfg.perturbation == …` chain). The standard prefix → K short-term re-sims + → distances loop then runs automatically. +- **Oracle families** (`relabel`, `rephase`, `case_route`, `mix_ratio`, + `label_swap`): a dedicated `_run__levels(...)` in `pipeline.py`, branched + early in `run_pipeline`, for post-hoc log transforms or custom sim loops. +- Both need: a `--perturbation` choice in `run_pipeline.py`, any new `cfg` + fields on `PipelineConfig`, a `DatasetSpec` in `datasets.py`, and tests under + `tests/evaluation/state_metrics/`. + +Recommended order: **#2 → #4 → #6** (increasing effort and decreasing +certainty). The planned **JSD distance** (see PERTURBATIONS / strategy notes) +helps #4 and #6 by avoiding Jaccard's ~0.9 real-log floor; consider it a +prerequisite for those two. + +## Plan — #2 Parallel-branch automation + +**Claim:** automating the *non-critical* branch of a parallel (AND) block leaves +cycle time (set by the critical path) and ρ on the critical resource unchanged, +but the non-critical activity stops occupying the active-instance set → +**state wins, `cycle_time` and `ngd_n2` blind.** + +- **Dataset (new):** `tools/generate_parallel_auto.py` (mirror + `generate_case_route.py`) emitting `synthetic_parallel_auto.{bpmn,json}`: + `Start → AND-split → { Critical (mean D_c) | NonCritical chain (Σ means = D_nc, with D_nc < D_c) } → AND-join → End`. + Critical path strictly dominates so cycle time = D_c regardless of the + non-critical branch. Same resource pool size on both branches, sized so + removing non-critical work does not change critical-resource ρ. + - Alternative quick start: reuse `samples/dev-samples/synthetic_and_k5.bpmn` + (1 parallel gateway) if a clean critical/non-critical split can be + identified; a purpose-built generator is cleaner for the paper. +- **Perturbation (new):** `build_branch_automation_params(base, *, automate_task_ids, level, out)` + scaling the non-critical task means by `(1 - level/100)` toward a small floor + (not exact 0 — keep the event in the trace so `ngd_n2` stays blind). `level=0` + no-op; `level=100` ≈ instantaneous. +- **Integration:** param family `parallel_auto`; dispatch branch in + `_prepare_params_for_level`; `cfg.automate_task_ids`; choice + `DatasetSpec`. +- **Invariants:** cycle time = D_c (constant while D_nc·(1-level/100) < D_c); + arrival/λ unchanged; ρ on the critical resource unchanged. +- **Expected result / assertion:** `state` (`activity`, `activity_case`, and the + time-weighted summary) c-index high and monotonic in `level`; `cycle_time` + and `ngd_n2` ≈ 0.5. Ranking via existing `ranking.py`. +- **Risks:** (a) Prosimos may drop zero-duration tasks from the log → keep a + duration floor so the label still appears (otherwise `ngd` would also move and + the "baseline blind" claim breaks). (b) If automation frees a resource, ρ can + shift — keep the non-critical pool separate / small so the critical ρ is + untouched. Verify ρ with the existing `_compute_utilization_rows`. +- **Effort:** medium (one generator + one builder + dispatch + dataset + test). + +## Plan — #4 Front-load vs back-load durations + +**Claim:** redistributing duration mass along a sequential chain (early-heavy vs +late-heavy) while holding the per-case total constant keeps aggregate cycle time +and ρ fixed, but moves *where in time* work sits → **state (time-weighted) +localizes it; `cycle_time` and `ngd_n2` blind.** (Honest caveat: `red`/`rtd` +likely co-detect, since event positions move — the differentiator is the +per-instant active-set view, not a marginal.) + +- **Dataset (new or reuse):** a linear chain of N comparable-duration activities + on one pool — generate `synthetic_linear_chain.{bpmn,json}` (N≈5) for a clean, + symmetric baseline; or reuse an existing sequential synthetic. +- **Perturbation (new):** `build_front_back_load_params(base, *, chain_task_ids, shift, out)` + reweighting task means by position so `Σ means` per case is invariant. Signed + `level`: `+` front-loads (early tasks longer), `−` back-loads. E.g. mean_i ·= + `1 + (level/100)·w_i` with `w_i` antisymmetric about the chain midpoint and + Σ(w_i·mean_i)=0 (renormalize to hold the total exactly). +- **Optional v2 (the notes' "swap resource capacity"):** also shift pool + capacity / calendars toward the heavy end so *instantaneous* ρ stays symmetric, + not just aggregate. Adds complexity; ship v1 (single pool, aggregate ρ held) + first. +- **Integration:** param family `front_back_load`; signed levels; dispatch + branch; `cfg.chain_task_ids`; choice + `DatasetSpec` (signed default levels). +- **Invariants:** per-case total duration (→ cycle time) constant; total work + (→ aggregate ρ) constant; λ unchanged. +- **Expected result / assertion:** `state` time-weighted distance monotonic in + `|level|` and roughly symmetric for front vs back; `cycle_time`, `ngd_n2` ≈ + 0.5; report `red`/`rtd` as partial co-detectors (supports the "complementary" + framing rather than a clean shutout). Prefer **JSD/cardinality** over Jaccard + for the summary here. +- **Risks:** deterministic durations may make the short-term re-sims too rigid; + keep distributional shapes. The active-overlap profile must actually change — + validate on a dry run before committing levels. +- **Effort:** medium-high (builder reweighting + total-invariance math + dataset; + v2 capacity-swap is extra). + +## Plan — #6 Real-life temporal case-type shift (optional / "icing") + +**Claim:** on a real log whose case-type mix drifts over time, the +`case_type`/`activity_type` projections track the composition shift while +`cycle_time`/`ngd_n2` stay flat. Observational, not a controlled perturbation. + +- **Not a builder** — this is data discovery + a measurement script. +- **Candidate logs/attributes:** BPIC-2017 (`ApplicationType`, `LoanGoal`), + BPIC-2012 (`AMOUNT_REQ` bucketed into red/blue), or `P2PFIN` (a categorical + case field). Need a *binary-izable* case attribute. +- **Step 1 — drift profiling:** bin cases by arrival time into windows; compute + the case-type marginal per window; quantify drift (total-variation distance + between windows). Select a log/attribute where drift is real and material. +- **Step 2 — experiment:** pick cutoffs in different drift regimes (or compare a + high-red vs high-blue window); run the existing short-term pipeline carrying + the real attribute as the `case_type` column (already supported by + `_load_prosimos_log` / `_write_prefix_csv`); measure projections vs baselines. +- **Integration:** new `evaluation/state_metrics/realworld_case_type.py` + + `DatasetSpec` entries mapping the real attribute column → `case_type`. +- **Risks:** (a) Jaccard ~0.9 floor on high-variability real logs → use the + planned JSD or `cardinality`. (b) A log with genuine temporal case-type drift + may not exist in our set — this is explicitly "icing on the cake," not + required for the core hypothesis. (c) No ground-truth control, so framing is + weaker than the synthetics. +- **Effort:** high uncertainty (data-dependent); do last, only if #2/#4 land. + +## Cross-cutting prerequisite + +`distances.py` currently has only `jaccard_multiset` + `cardinality`. Adding a +**JSD / distribution distance** (strategy note: prefer JSD over Jaccard; Maksym's +Jaccard-sensitivity concern) should precede #4 and #6, since both risk the +Jaccard real-log floor. Small, self-contained: `distances.py` + wire into +`api.py` summary + `ranking.py` + a test. diff --git a/evaluation/state_metrics/datasets.py b/evaluation/state_metrics/datasets.py index 5b0053a..f701c34 100644 --- a/evaluation/state_metrics/datasets.py +++ b/evaluation/state_metrics/datasets.py @@ -20,6 +20,10 @@ class DatasetSpec: # Ladder used when --perturbation duration is selected. Values are # integer percentages, mapped by the pipeline to scale = 1 + level/100. default_duration_levels: tuple[int, ...] = (0, 25, 50, 100, 200, 300) + # parallel_auto: the non-critical AND-branch tasks to automate. + automate_task_ids: tuple[str, ...] = () + # front_back_load: the ordered sequential-chain tasks to reweight. + chain_task_ids: tuple[str, ...] = () DATASETS: dict[str, DatasetSpec] = { @@ -35,6 +39,29 @@ class DatasetSpec: 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 + # (t_nc1..t_nc3) are the automation targets for the `parallel_auto` family. + "synthetic_parallel_auto": DatasetSpec( + name="synthetic_parallel_auto", + bpmn=REPO_ROOT / "samples" / "dev-samples" / "synthetic_parallel_auto.bpmn", + params=REPO_ROOT / "samples" / "dev-samples" / "synthetic_parallel_auto.json", + remove_from_profile="NonCritical", + default_levels=(0, 25, 50, 75, 100), + automate_task_ids=("t_nc1", "t_nc2", "t_nc3"), + ), + # Linear-chain synthetic (scenario #4): a duration-symmetric sequence of + # five steps on one pool. Generated by tools/generate_linear_chain.py. The + # `front_back_load` family reweights t_step1..t_step5 total-invariantly. + "synthetic_linear_chain": DatasetSpec( + name="synthetic_linear_chain", + bpmn=REPO_ROOT / "samples" / "dev-samples" / "synthetic_linear_chain.bpmn", + params=REPO_ROOT / "samples" / "dev-samples" / "synthetic_linear_chain.json", + remove_from_profile="Worker", + default_levels=(0, 20, 40, 60, 80), + chain_task_ids=("t_step1", "t_step2", "t_step3", "t_step4", "t_step5"), + ), "synthetic_xor_loop": DatasetSpec( name="synthetic_xor_loop", bpmn=REPO_ROOT / "samples" / "dev-samples" / "synthetic_xor_loop.bpmn", diff --git a/evaluation/state_metrics/perturb.py b/evaluation/state_metrics/perturb.py index 5b2e972..212bf84 100644 --- a/evaluation/state_metrics/perturb.py +++ b/evaluation/state_metrics/perturb.py @@ -642,6 +642,172 @@ def _is_split(entry: dict) -> bool: } +def build_branch_automation_params( + base_json_path: str | Path, + *, + automate_task_ids: list[str], + level: int, + floor_seconds: float = 1.0, + out_json_path: str | Path, +) -> dict: + """Automate (shrink) the durations of the non-critical branch tasks. + + Designed for the *parallel-automation* synthetic (see + ``tools/generate_parallel_auto.py``): the listed ``automate_task_ids`` are + the non-critical AND branch, whose total duration is dominated by the + critical branch. Each task's duration params are scaled by + ``(1 - level/100)``, with the location parameter floored at + ``floor_seconds`` so the activity still appears once per case (keeping the + bigram histogram and per-case activity counts identical, hence ``ngd_n2`` + blind). Because the critical branch sets the cycle time and runs on a + separate pool, ``cycle_time`` is blind too — only the time-weighted state + metric sees the non-critical branch shrink out of the concurrent active set. + + ``level == 0`` is a no-op (factor 1.0). ``level`` is read as a percentage in + ``[0, 100]``; ``level == 100`` collapses every automated task to the floor. + + Returns a manifest with the scale factor and which tasks were touched. + """ + if level < 0: + raise ValueError("level must be >= 0") + if level > 100: + raise ValueError("level must be <= 100") + if not automate_task_ids: + raise ValueError("automate_task_ids must be non-empty") + if floor_seconds <= 0: + raise ValueError("floor_seconds must be > 0") + + params = _load_params(base_json_path) + factor = 1.0 - level / 100.0 + targets = set(automate_task_ids) + touched: list[str] = [] + for task in params.get("task_resource_distribution", []): + if task.get("task_id") not in targets: + continue + touched.append(task["task_id"]) + for r in task.get("resources", []): + dparams = r.get("distribution_params", []) + new_params = [] + for i, p in enumerate(dparams): + scaled = p["value"] * factor + # Floor only the location/value parameter (index 0) so the + # activity keeps a strictly positive duration and stays in the + # trace; scale any shape/min/max params by the same factor. + if i == 0: + scaled = max(scaled, floor_seconds) + new_params.append({"value": scaled}) + r["distribution_params"] = new_params + + missing = targets - set(touched) + if missing: + raise ValueError( + f"automate_task_ids not found in task_resource_distribution: {sorted(missing)}" + ) + + _write_params(params, out_json_path) + return { + "automate_task_ids": list(automate_task_ids), + "level": level, + "factor": factor, + "floor_seconds": floor_seconds, + "tasks_touched": touched, + } + + +def build_front_back_load_params( + base_json_path: str | Path, + *, + chain_task_ids: list[str], + shift: int, + out_json_path: str | Path, +) -> dict: + """Redistribute duration mass along a sequential chain, total-invariant. + + Designed for the *linear-chain* synthetic (see + ``tools/generate_linear_chain.py``): the ordered ``chain_task_ids`` form a + sequence of comparable-duration activities on one pool. This builder + reweights their mean durations by position so the per-case **total** + duration (and hence cycle time and aggregate utilisation) is held constant, + but the duration mass moves toward the front or the back of the chain. + + Signed ``shift`` (a percentage): + * ``shift > 0`` *front-loads* — early tasks longer, late tasks shorter. + * ``shift < 0`` *back-loads* — late tasks longer, early tasks shorter. + * ``shift == 0`` is a no-op. + + For a chain of ``n`` tasks at ordered positions ``p_i = i/(n-1) ∈ [0, 1]`` + the per-task weight is ``w_i = 1 + (shift/100)·(1 - 2·p_i)`` — an + antisymmetric ramp about the chain midpoint. New means are ``m_i·w_i``, + then globally renormalised by ``Σm_i / Σ(m_i·w_i)`` so the total is held + **exactly** regardless of any base-mean asymmetry. + + Because only *where* the duration sits changes (not the per-case total, the + activities, the bigrams, or aggregate ρ), ``cycle_time`` and ``ngd_n2`` are + blind, while the time-weighted state metric localises the moved mass. + ``|shift|`` must be < 100 so every weight stays positive. + + Returns a manifest with the per-task factors actually applied. + """ + if abs(shift) >= 100: + raise ValueError("|shift| must be < 100 so all task weights stay positive") + if len(chain_task_ids) < 2: + raise ValueError("chain_task_ids must list at least two tasks (ordered)") + + params = _load_params(base_json_path) + trd_by_id = {t.get("task_id"): t for t in params.get("task_resource_distribution", [])} + missing = [tid for tid in chain_task_ids if tid not in trd_by_id] + if missing: + raise ValueError(f"chain_task_ids not found in params: {missing}") + + n = len(chain_task_ids) + s = shift / 100.0 + + def _task_mean(task: dict) -> float: + # Representative mean = first resource's location parameter. + resources = task.get("resources", []) + if not resources or not resources[0].get("distribution_params"): + raise ValueError(f"task {task.get('task_id')!r} has no distribution params") + return float(resources[0]["distribution_params"][0]["value"]) + + if shift == 0: + _write_params(params, out_json_path) + return { + "chain_task_ids": list(chain_task_ids), "shift": 0, + "factors": {tid: 1.0 for tid in chain_task_ids}, + "total_invariant": True, + } + + weights: dict[str, float] = {} + for i, tid in enumerate(chain_task_ids): + p_i = i / (n - 1) + weights[tid] = 1.0 + s * (1.0 - 2.0 * p_i) + + base_total = sum(_task_mean(trd_by_id[tid]) for tid in chain_task_ids) + new_total = sum(_task_mean(trd_by_id[tid]) * weights[tid] for tid in chain_task_ids) + g = base_total / new_total if new_total > 0 else 1.0 + + factors: dict[str, float] = {} + for tid in chain_task_ids: + f = weights[tid] * g + factors[tid] = f + for r in trd_by_id[tid].get("resources", []): + r["distribution_params"] = [ + {"value": p["value"] * f} for p in r.get("distribution_params", []) + ] + + _write_params(params, out_json_path) + return { + "chain_task_ids": list(chain_task_ids), + "shift": shift, + "factors": factors, + "base_total_mean": base_total, + "renormalised_total_mean": sum( + _task_mean(trd_by_id[tid]) for tid in chain_task_ids + ), + "total_invariant": True, + } + + def build_arrival_burstier_params( base_json_path: str | Path, *, diff --git a/evaluation/state_metrics/pipeline.py b/evaluation/state_metrics/pipeline.py index ec068f4..88ceec1 100644 --- a/evaluation/state_metrics/pipeline.py +++ b/evaluation/state_metrics/pipeline.py @@ -60,9 +60,11 @@ from evaluation.state_metrics.perturb import ( build_all_calendars_shifted_params, build_arrival_burstier_params, + build_branch_automation_params, build_calendar_shifted_params, build_case_route_params, build_duration_scaled_params, + build_front_back_load_params, build_gateway_biased_params, build_perturbed_params, build_role_swap_params, @@ -146,6 +148,15 @@ class PipelineConfig: # `level` is then the percentage of case_type tags swapped on that # reference (the pairing-unique attack on a genuinely attribute-routed log). case_route_ruled: int | None = None + # parallel_auto kwargs (scenario #2). The non-critical AND-branch tasks to + # automate; `level` is read as the percent shrink toward `automation_floor`. + automate_task_ids: tuple[str, ...] | None = None + automation_floor_seconds: float = 1.0 + # front_back_load kwargs (scenario #4). The ordered chain tasks whose mean + # durations are reweighted total-invariantly; `load_direction` decides the + # sign so the (non-negative) `level` ladder stays monotone for ranking. + chain_task_ids: tuple[str, ...] | None = None + load_direction: str = "front" # "front" or "back" # Windowing controls (added 2026-05 to enable cross-utilization comparison). # cutoff_strategy: # "p90_wip" — middle of top-decile WIP band (legacy default). @@ -804,6 +815,37 @@ def _prepare_params_for_level( cv2_multiplier=cv2_mult, out_json_path=out_params, ) + elif cfg.perturbation == "parallel_auto": + if cfg.automate_task_ids is None: + raise ValueError("parallel_auto requires cfg.automate_task_ids") + if level < 0: + raise ValueError("parallel_auto levels must be >= 0 (percent automated)") + manifest = build_branch_automation_params( + cfg.params_path, + automate_task_ids=list(cfg.automate_task_ids), + level=level, + floor_seconds=cfg.automation_floor_seconds, + out_json_path=out_params, + ) + elif cfg.perturbation == "front_back_load": + if cfg.chain_task_ids is None: + raise ValueError("front_back_load requires cfg.chain_task_ids") + if level < 0: + raise ValueError( + "front_back_load levels must be >= 0; direction is set by " + "cfg.load_direction" + ) + if cfg.load_direction not in ("front", "back"): + raise ValueError("load_direction must be 'front' or 'back'") + # Non-negative ladder stays monotone for the c-index; the sign of the + # shift is taken from the configured direction. + signed = level if cfg.load_direction == "front" else -level + manifest = build_front_back_load_params( + cfg.params_path, + chain_task_ids=list(cfg.chain_task_ids), + shift=signed, + out_json_path=out_params, + ) else: raise ValueError(f"unknown perturbation type {cfg.perturbation!r}") manifest = {"perturbation": cfg.perturbation, "level": level, **manifest} @@ -1136,6 +1178,71 @@ def flip(row_case_id: str, row_type: str) -> str: return out +def drift_case_types( + log: pd.DataFrame, + *, + strength: float, + rng: np.random.Generator, +) -> pd.DataFrame: + """Return a copy of ``log`` whose ``case_type`` tags are made to *drift* + along the arrival timeline, with the global marginal preserved exactly. + + A fraction ``strength`` of the cases (chosen at random, by arrival + position) have their tags re-sorted so that — among the chosen positions — + the lexicographically-smaller value lands on the earlier arrivals and the + larger value on the later ones. Because the re-sort only permutes the tags + *already present* on the chosen positions, the per-value counts (and hence + the global red/blue marginal) are unchanged; only the temporal arrangement + shifts. At ``strength == 1`` the chosen set is the whole log and the tags + are fully time-sorted; at ``strength == 0`` the output is identical. + + This is the controlled, synthetic analogue of a real log whose case-type + mix drifts over time (scenario #6). Unlike ``label_swap_case_types`` (random + marginal-preserving swaps → only the *paired* ``activity_type`` projection + moves), a temporal drift skews the active case_type composition inside any + sub-window, so the marginal-blind baselines stay flat while BOTH the + ``case_type`` and ``activity_type`` state projections detect it. + + The event stream (activities, timestamps, resources, case ids) is untouched, + so cycle_time / ngd / red / rtd and the type-agnostic state projections are + invariant by construction. + """ + out = log.copy() + if "case_type" not in out.columns: + raise ValueError("log has no 'case_type' column to drift") + if not 0.0 <= strength <= 1.0: + raise ValueError("strength must be in [0, 1]") + if strength <= 0: + return out + + first_start = out.groupby("case_id")["start_time"].min().sort_values() + case_order = list(first_start.index) + tag_by_case = out.groupby("case_id")["case_type"].first().to_dict() + n = len(case_order) + k = int(round(strength * n)) + if k <= 1: + return out + + # Choose k arrival positions to re-sort; keep them in arrival order. + positions = np.sort(rng.choice(n, size=k, replace=False)) + chosen_cases = [case_order[p] for p in positions] + chosen_tags = [tag_by_case[c] for c in chosen_cases] + distinct = sorted(set(t for t in chosen_tags if t is not None and t == t)) + if len(distinct) < 2: + return out # only one value among the chosen — nothing to drift + early_val = distinct[0] + n_early = sum(t == early_val for t in chosen_tags) + # early_val first (earliest arrivals), everything else after. + resorted = [early_val] * n_early + [ + t for t in chosen_tags if t != early_val + ] + new_tag = dict(tag_by_case) + for case, nt in zip(chosen_cases, resorted): + new_tag[case] = nt + out["case_type"] = out["case_id"].map(new_tag) + return out + + def _run_label_swap_levels( cfg: PipelineConfig, run_dir: Path, @@ -1437,6 +1544,87 @@ def _run_case_route_levels( )) +def _run_case_type_drift_levels( + cfg: PipelineConfig, + run_dir: Path, + *, + prefix_csv: Path, + cutoff: pd.Timestamp, + horizon: pd.Timedelta, + horizon_end: pd.Timestamp, + ongoing_ids: set[str], + results: list[dict], + util_rows: list[dict], +) -> None: + """Case-type-drift oracle (scenario #6, controlled synthetic form). + + Like ``_run_case_route_levels`` the reference is a REAL short-term sim in + which ``case_type`` genuinely drives XOR routing (red -> A-branch, blue -> + B-branch) via Prosimos branch_rules. Each ``level`` then makes the + ``case_type`` tags *drift* along the arrival timeline at strength + ``level%`` (``drift_case_types``), holding the global red/blue marginal + fixed. + + Because the event stream is untouched and the marginal is preserved, + cycle_time / ngd_n2 / red / rtd and the type-agnostic state projections are + blind. Unlike the random label_swap, a *temporal* drift skews the active + case_type composition inside the evaluation window, so BOTH the + ``case_type`` and the joint ``activity_type`` state projections detect it — + the data-attribute, drift-over-time win. + """ + n_splits = _count_split_gateways(Path(cfg.params_path)) + n_ruled = cfg.case_route_ruled if cfg.case_route_ruled is not None else n_splits + ruled_params = run_dir / "case_route_ruled.json" + route_manifest = build_case_route_params( + cfg.params_path, n_gateways_ruled=n_ruled, out_json_path=ruled_params, + ) + with open(ruled_params, encoding="utf-8") as f: + level_params = json.load(f) + role_map = _resource_to_profile(level_params) + + ref_logs: list[pd.DataFrame] = [] + for k in range(1, cfg.runs + 1): + print(f"[case_type_drift] reference (real attribute-routed) sim k={k}/{cfg.runs}") + ref = _run_short_term( + prefix_csv=prefix_csv, bpmn=cfg.bpmn_path, params=ruled_params, + cutoff=cutoff, horizon_end=horizon_end, + total_cases=cfg.sim_total_cases, + out_dir=run_dir / "reference" / f"k_{k}", + seed=10_000 + k, + ) + if "case_type" not in ref.columns: + raise RuntimeError( + "reference sim has no case_type column; the params must declare " + "a case_type case attribute" + ) + ref_logs.append(ref) + + (run_dir / "case_type_drift_manifest.json").write_text(json.dumps({ + **route_manifest, + "levels_are_percent_drift_strength": True, + "reference": "real short-term sim with case_type-driven branch_rules", + }, indent=2)) + + for level in cfg.levels: + strength = level / 100.0 + for k in range(1, cfg.runs + 1): + print(f"[case_type_drift] drift level={level}% k={k}/{cfg.runs}") + ref = ref_logs[k - 1] + rng = np.random.default_rng(70_000 + 1_000 * level + k) + perturbed = drift_case_types(ref, strength=strength, rng=rng) + base_A = filter_to_cases_in_window(ref, ongoing_ids, cutoff, horizon) + sim_A = filter_to_cases_in_window(perturbed, ongoing_ids, cutoff, horizon) + util_rows.extend(_compute_utilization_rows( + sim_log=ref, params=level_params, + cutoff=cutoff, horizon_end=horizon_end, level=level, k_sim=k, + )) + results.extend(_compute_metrics_row( + baseline_continuation=base_A, sim_continuation=sim_A, + level=level, k_baseline=k, k_sim=k, scope="A_ongoing", + window=(cutoff, horizon_end), role_map=role_map, + )) + + def run_pipeline(cfg: PipelineConfig) -> Path: """Run the end-to-end pipeline; return path to the results.csv.""" run_id = generate_short_uuid() @@ -1523,6 +1711,13 @@ def run_pipeline(cfg: PipelineConfig) -> Path: results=results, util_rows=util_rows, ) return _finalize_pipeline_outputs(run_dir, results, util_rows) + if cfg.perturbation == "case_type_drift": + _run_case_type_drift_levels( + cfg, run_dir, prefix_csv=prefix_csv, cutoff=cutoff, + horizon=horizon, horizon_end=horizon_end, ongoing_ids=ongoing_ids, + results=results, util_rows=util_rows, + ) + return _finalize_pipeline_outputs(run_dir, results, util_rows) # Step 3: K short-term BASELINE runs at level 0 (shared across all levels). # The level-0 short-term sim is the locked baseline: zero-perturbation diff --git a/evaluation/state_metrics/run_pipeline.py b/evaluation/state_metrics/run_pipeline.py index 6930dbb..2aab0ee 100644 --- a/evaluation/state_metrics/run_pipeline.py +++ b/evaluation/state_metrics/run_pipeline.py @@ -52,7 +52,9 @@ def main() -> None: choices=["resources", "duration", "role_swap", "calendar_shift", "calendar_shift_all", "gateway", "arrival_burst", "relabel", "rephase", - "mix_ratio", "label_swap", "case_route"], + "mix_ratio", "label_swap", "case_route", + "parallel_auto", "front_back_load", + "case_type_drift"], default="resources", help="which perturbation family to apply") p.add_argument("--remove-from-profile", default=None, @@ -82,8 +84,11 @@ def main() -> None: help="mix_ratio: baseline fraction of green cases (the " "reference mix). Default 0.5.") p.add_argument("--case-route-ruled", type=int, default=None, - help="case_route: number of XOR splits the reference sim " - "routes by case_type (default: all splits)") + help="case_route / case_type_drift: number of XOR splits the " + "reference sim routes by case_type (default: all splits)") + p.add_argument("--load-direction", choices=["front", "back"], default="front", + help="front_back_load: which end of the chain gets the " + "duration mass (default: front)") p.add_argument("--gt-total-cases", type=int, default=2000) p.add_argument("--sim-total-cases", type=int, default=2000) p.add_argument("--outputs-root", type=Path, @@ -150,6 +155,15 @@ def main() -> None: elif args.perturbation == "case_route": # Percentage of case_type tags swapped on a real attribute-routed sim. levels = (0, 10, 20, 30, 40) + elif args.perturbation == "case_type_drift": + # Percentage drift strength of case_type tags along the timeline. + levels = (0, 25, 50, 75, 100) + elif args.perturbation == "parallel_auto": + # Percentage automation (duration shrink) of the non-critical branch. + levels = spec.default_levels + elif args.perturbation == "front_back_load": + # Percentage of duration mass moved toward the chosen chain end. + levels = spec.default_levels else: levels = spec.default_levels @@ -177,6 +191,9 @@ def main() -> None: mix_red_params=args.mix_red_params, mix_baseline_green=args.mix_baseline_green, case_route_ruled=args.case_route_ruled, + automate_task_ids=spec.automate_task_ids or None, + chain_task_ids=spec.chain_task_ids or None, + load_direction=args.load_direction, ) run_pipeline(cfg) diff --git a/samples/dev-samples/synthetic_linear_chain.bpmn b/samples/dev-samples/synthetic_linear_chain.bpmn new file mode 100644 index 0000000..fb42828 --- /dev/null +++ b/samples/dev-samples/synthetic_linear_chain.bpmn @@ -0,0 +1 @@ +f_startf_startf_step1f_step1f_step2f_step2f_step3f_step3f_step4f_step4f_endf_end \ No newline at end of file diff --git a/samples/dev-samples/synthetic_linear_chain.json b/samples/dev-samples/synthetic_linear_chain.json new file mode 100644 index 0000000..d760387 --- /dev/null +++ b/samples/dev-samples/synthetic_linear_chain.json @@ -0,0 +1,2438 @@ +{ + "task_resource_distribution": [ + { + "task_id": "t_step1", + "resources": [ + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_1" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_2" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_3" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_4" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_5" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_6" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_7" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_8" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_9" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_10" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_11" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_12" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_13" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_14" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_15" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_16" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_17" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_18" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_19" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_20" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_21" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_22" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_23" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_24" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_25" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_26" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_27" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_28" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_29" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_30" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_31" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_32" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_33" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_34" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_35" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_36" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_37" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_38" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_39" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_40" + } + ] + }, + { + "task_id": "t_step2", + "resources": [ + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_1" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_2" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_3" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_4" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_5" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_6" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_7" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_8" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_9" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_10" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_11" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_12" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_13" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_14" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_15" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_16" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_17" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_18" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_19" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_20" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_21" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_22" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_23" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_24" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_25" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_26" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_27" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_28" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_29" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_30" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_31" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_32" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_33" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_34" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_35" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_36" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_37" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_38" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_39" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_40" + } + ] + }, + { + "task_id": "t_step3", + "resources": [ + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_1" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_2" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_3" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_4" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_5" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_6" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_7" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_8" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_9" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_10" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_11" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_12" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_13" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_14" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_15" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_16" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_17" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_18" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_19" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_20" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_21" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_22" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_23" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_24" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_25" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_26" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_27" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_28" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_29" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_30" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_31" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_32" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_33" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_34" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_35" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_36" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_37" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_38" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_39" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_40" + } + ] + }, + { + "task_id": "t_step4", + "resources": [ + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_1" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_2" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_3" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_4" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_5" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_6" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_7" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_8" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_9" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_10" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_11" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_12" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_13" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_14" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_15" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_16" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_17" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_18" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_19" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_20" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_21" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_22" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_23" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_24" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_25" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_26" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_27" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_28" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_29" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_30" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_31" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_32" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_33" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_34" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_35" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_36" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_37" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_38" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_39" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_40" + } + ] + }, + { + "task_id": "t_step5", + "resources": [ + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_1" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_2" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_3" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_4" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_5" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_6" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_7" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_8" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_9" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_10" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_11" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_12" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_13" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_14" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_15" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_16" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_17" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_18" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_19" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_20" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_21" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_22" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_23" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_24" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_25" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_26" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_27" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_28" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_29" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_30" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_31" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_32" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_33" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_34" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_35" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_36" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_37" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_38" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_39" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 180.0 + } + ], + "resource_id": "Worker_40" + } + ] + } + ], + "resource_calendars": [ + { + "id": "cal_247", + "name": "24/7", + "time_periods": [ + { + "from": "MONDAY", + "to": "SUNDAY", + "beginTime": "00:00:00", + "endTime": "23:59:59.999" + } + ] + } + ], + "gateway_branching_probabilities": [], + "arrival_time_distribution": { + "distribution_name": "expon", + "distribution_params": [ + { + "value": 90.0 + }, + { + "value": 0 + }, + { + "value": 3600 + } + ] + }, + "arrival_time_calendar": [ + { + "from": "MONDAY", + "to": "SUNDAY", + "beginTime": "00:00:00", + "endTime": "23:59:59.999" + } + ], + "resource_profiles": [ + { + "id": "Worker_profile", + "name": "Worker", + "resource_list": [ + { + "id": "Worker_1", + "name": "Worker_1", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_2", + "name": "Worker_2", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_3", + "name": "Worker_3", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_4", + "name": "Worker_4", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_5", + "name": "Worker_5", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_6", + "name": "Worker_6", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_7", + "name": "Worker_7", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_8", + "name": "Worker_8", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_9", + "name": "Worker_9", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_10", + "name": "Worker_10", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_11", + "name": "Worker_11", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_12", + "name": "Worker_12", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_13", + "name": "Worker_13", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_14", + "name": "Worker_14", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_15", + "name": "Worker_15", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_16", + "name": "Worker_16", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_17", + "name": "Worker_17", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_18", + "name": "Worker_18", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_19", + "name": "Worker_19", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_20", + "name": "Worker_20", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_21", + "name": "Worker_21", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_22", + "name": "Worker_22", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_23", + "name": "Worker_23", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_24", + "name": "Worker_24", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_25", + "name": "Worker_25", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_26", + "name": "Worker_26", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_27", + "name": "Worker_27", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_28", + "name": "Worker_28", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_29", + "name": "Worker_29", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_30", + "name": "Worker_30", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_31", + "name": "Worker_31", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_32", + "name": "Worker_32", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_33", + "name": "Worker_33", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_34", + "name": "Worker_34", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_35", + "name": "Worker_35", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_36", + "name": "Worker_36", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_37", + "name": "Worker_37", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_38", + "name": "Worker_38", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_39", + "name": "Worker_39", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + }, + { + "id": "Worker_40", + "name": "Worker_40", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_step1", + "t_step2", + "t_step3", + "t_step4", + "t_step5" + ] + } + ] + } + ], + "event_distribution": [], + "batch_processing": [], + "case_attributes": [], + "prioritisation_rules": [] +} \ No newline at end of file diff --git a/samples/dev-samples/synthetic_parallel_auto.bpmn b/samples/dev-samples/synthetic_parallel_auto.bpmn new file mode 100644 index 0000000..5da1833 --- /dev/null +++ b/samples/dev-samples/synthetic_parallel_auto.bpmn @@ -0,0 +1 @@ +f_startf_startf_reg_splitf_reg_splitf_split_critf_split_nc0f_split_critf_crit_joinf_split_nc0f_nc1f_nc1f_nc2f_nc2f_nc_joinf_crit_joinf_nc_joinf_join_decf_join_decf_dec_endf_dec_end \ No newline at end of file diff --git a/samples/dev-samples/synthetic_parallel_auto.json b/samples/dev-samples/synthetic_parallel_auto.json new file mode 100644 index 0000000..78eea1a --- /dev/null +++ b/samples/dev-samples/synthetic_parallel_auto.json @@ -0,0 +1,2763 @@ +{ + "task_resource_distribution": [ + { + "task_id": "t_reg", + "resources": [ + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_1" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_2" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_3" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_4" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_5" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_6" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_7" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_8" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_9" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_10" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_11" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_12" + } + ] + }, + { + "task_id": "t_dec", + "resources": [ + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_1" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_2" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_3" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_4" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_5" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_6" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_7" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_8" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_9" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_10" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_11" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 60.0 + } + ], + "resource_id": "Common_12" + } + ] + }, + { + "task_id": "t_crit", + "resources": [ + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_1" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_2" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_3" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_4" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_5" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_6" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_7" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_8" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_9" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_10" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_11" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_12" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_13" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_14" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_15" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_16" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_17" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_18" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_19" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_20" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_21" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_22" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_23" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_24" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_25" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_26" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_27" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_28" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_29" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_30" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_31" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_32" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_33" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_34" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_35" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_36" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_37" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_38" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_39" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 600.0 + } + ], + "resource_id": "Critical_40" + } + ] + }, + { + "task_id": "t_nc1", + "resources": [ + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_1" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_2" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_3" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_4" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_5" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_6" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_7" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_8" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_9" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_10" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_11" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_12" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_13" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_14" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_15" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_16" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_17" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_18" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_19" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_20" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_21" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_22" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_23" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_24" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_25" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_26" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_27" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_28" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_29" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_30" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_31" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_32" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_33" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_34" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_35" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_36" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_37" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_38" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_39" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_40" + } + ] + }, + { + "task_id": "t_nc2", + "resources": [ + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_1" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_2" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_3" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_4" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_5" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_6" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_7" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_8" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_9" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_10" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_11" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_12" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_13" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_14" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_15" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_16" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_17" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_18" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_19" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_20" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_21" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_22" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_23" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_24" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_25" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_26" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_27" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_28" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_29" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_30" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_31" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_32" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_33" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_34" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_35" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_36" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_37" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_38" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_39" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_40" + } + ] + }, + { + "task_id": "t_nc3", + "resources": [ + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_1" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_2" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_3" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_4" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_5" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_6" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_7" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_8" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_9" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_10" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_11" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_12" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_13" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_14" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_15" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_16" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_17" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_18" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_19" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_20" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_21" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_22" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_23" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_24" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_25" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_26" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_27" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_28" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_29" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_30" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_31" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_32" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_33" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_34" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_35" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_36" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_37" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_38" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_39" + }, + { + "distribution_name": "fix", + "distribution_params": [ + { + "value": 120.0 + } + ], + "resource_id": "NonCritical_40" + } + ] + } + ], + "resource_calendars": [ + { + "id": "cal_247", + "name": "24/7", + "time_periods": [ + { + "from": "MONDAY", + "to": "SUNDAY", + "beginTime": "00:00:00", + "endTime": "23:59:59.999" + } + ] + } + ], + "gateway_branching_probabilities": [], + "arrival_time_distribution": { + "distribution_name": "expon", + "distribution_params": [ + { + "value": 30.0 + }, + { + "value": 0 + }, + { + "value": 3600 + } + ] + }, + "arrival_time_calendar": [ + { + "from": "MONDAY", + "to": "SUNDAY", + "beginTime": "00:00:00", + "endTime": "23:59:59.999" + } + ], + "resource_profiles": [ + { + "id": "Common_profile", + "name": "Common", + "resource_list": [ + { + "id": "Common_1", + "name": "Common_1", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + }, + { + "id": "Common_2", + "name": "Common_2", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + }, + { + "id": "Common_3", + "name": "Common_3", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + }, + { + "id": "Common_4", + "name": "Common_4", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + }, + { + "id": "Common_5", + "name": "Common_5", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + }, + { + "id": "Common_6", + "name": "Common_6", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + }, + { + "id": "Common_7", + "name": "Common_7", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + }, + { + "id": "Common_8", + "name": "Common_8", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + }, + { + "id": "Common_9", + "name": "Common_9", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + }, + { + "id": "Common_10", + "name": "Common_10", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + }, + { + "id": "Common_11", + "name": "Common_11", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + }, + { + "id": "Common_12", + "name": "Common_12", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_reg", + "t_dec" + ] + } + ] + }, + { + "id": "Critical_profile", + "name": "Critical", + "resource_list": [ + { + "id": "Critical_1", + "name": "Critical_1", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_2", + "name": "Critical_2", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_3", + "name": "Critical_3", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_4", + "name": "Critical_4", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_5", + "name": "Critical_5", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_6", + "name": "Critical_6", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_7", + "name": "Critical_7", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_8", + "name": "Critical_8", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_9", + "name": "Critical_9", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_10", + "name": "Critical_10", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_11", + "name": "Critical_11", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_12", + "name": "Critical_12", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_13", + "name": "Critical_13", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_14", + "name": "Critical_14", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_15", + "name": "Critical_15", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_16", + "name": "Critical_16", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_17", + "name": "Critical_17", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_18", + "name": "Critical_18", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_19", + "name": "Critical_19", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_20", + "name": "Critical_20", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_21", + "name": "Critical_21", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_22", + "name": "Critical_22", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_23", + "name": "Critical_23", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_24", + "name": "Critical_24", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_25", + "name": "Critical_25", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_26", + "name": "Critical_26", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_27", + "name": "Critical_27", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_28", + "name": "Critical_28", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_29", + "name": "Critical_29", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_30", + "name": "Critical_30", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_31", + "name": "Critical_31", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_32", + "name": "Critical_32", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_33", + "name": "Critical_33", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_34", + "name": "Critical_34", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_35", + "name": "Critical_35", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_36", + "name": "Critical_36", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_37", + "name": "Critical_37", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_38", + "name": "Critical_38", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_39", + "name": "Critical_39", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + }, + { + "id": "Critical_40", + "name": "Critical_40", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_crit" + ] + } + ] + }, + { + "id": "NonCritical_profile", + "name": "NonCritical", + "resource_list": [ + { + "id": "NonCritical_1", + "name": "NonCritical_1", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_2", + "name": "NonCritical_2", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_3", + "name": "NonCritical_3", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_4", + "name": "NonCritical_4", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_5", + "name": "NonCritical_5", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_6", + "name": "NonCritical_6", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_7", + "name": "NonCritical_7", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_8", + "name": "NonCritical_8", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_9", + "name": "NonCritical_9", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_10", + "name": "NonCritical_10", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_11", + "name": "NonCritical_11", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_12", + "name": "NonCritical_12", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_13", + "name": "NonCritical_13", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_14", + "name": "NonCritical_14", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_15", + "name": "NonCritical_15", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_16", + "name": "NonCritical_16", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_17", + "name": "NonCritical_17", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_18", + "name": "NonCritical_18", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_19", + "name": "NonCritical_19", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_20", + "name": "NonCritical_20", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_21", + "name": "NonCritical_21", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_22", + "name": "NonCritical_22", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_23", + "name": "NonCritical_23", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_24", + "name": "NonCritical_24", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_25", + "name": "NonCritical_25", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_26", + "name": "NonCritical_26", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_27", + "name": "NonCritical_27", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_28", + "name": "NonCritical_28", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_29", + "name": "NonCritical_29", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_30", + "name": "NonCritical_30", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_31", + "name": "NonCritical_31", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_32", + "name": "NonCritical_32", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_33", + "name": "NonCritical_33", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_34", + "name": "NonCritical_34", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_35", + "name": "NonCritical_35", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_36", + "name": "NonCritical_36", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_37", + "name": "NonCritical_37", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_38", + "name": "NonCritical_38", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_39", + "name": "NonCritical_39", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + }, + { + "id": "NonCritical_40", + "name": "NonCritical_40", + "cost_per_hour": 0, + "amount": 1, + "calendar": "cal_247", + "assignedTasks": [ + "t_nc1", + "t_nc2", + "t_nc3" + ] + } + ] + } + ], + "event_distribution": [], + "batch_processing": [], + "case_attributes": [], + "prioritisation_rules": [] +} \ No newline at end of file diff --git a/tests/evaluation/state_metrics/test_perturb.py b/tests/evaluation/state_metrics/test_perturb.py index 8de29e5..ff9f3f3 100644 --- a/tests/evaluation/state_metrics/test_perturb.py +++ b/tests/evaluation/state_metrics/test_perturb.py @@ -5,19 +5,31 @@ from evaluation.state_metrics.perturb import ( build_arrival_burstier_params, + build_branch_automation_params, build_calendar_shifted_params, build_case_route_params, + build_front_back_load_params, build_gateway_biased_params, build_perturbed_params, build_role_swap_params, ) -XOR_JSON = Path(__file__).resolve().parents[3] / "samples" / "dev-samples" / "synthetic_xor_loop.json" +_DEV = Path(__file__).resolve().parents[3] / "samples" / "dev-samples" +XOR_JSON = _DEV / "synthetic_xor_loop.json" LOAN_JSON = Path(__file__).resolve().parents[3] / "samples" / "icpm-2025" / "synthetic" / "Loan-stable.json" -CASE_ROUTE_JSON = ( - Path(__file__).resolve().parents[3] / "samples" / "dev-samples" / "synthetic_case_route.json" -) +CASE_ROUTE_JSON = _DEV / "synthetic_case_route.json" +PARALLEL_AUTO_JSON = _DEV / "synthetic_parallel_auto.json" +LINEAR_CHAIN_JSON = _DEV / "synthetic_linear_chain.json" + + +def _task_mean(data, task_id): + t = next(t for t in data["task_resource_distribution"] if t["task_id"] == task_id) + return t["resources"][0]["distribution_params"][0]["value"] + + +def _chain_total_mean(data, task_ids): + return sum(_task_mean(data, t) for t in task_ids) @pytest.mark.skipif(not CASE_ROUTE_JSON.exists(), reason="synthetic_case_route.json missing") @@ -438,3 +450,130 @@ def test_arrival_burst_rejects_non_gamma(tmp_path): build_arrival_burstier_params( src, cv2_multiplier=2.0, out_json_path=tmp_path / "dst.json", ) + + +# --------------------------------------------------------------------------- +# parallel_auto: branch automation (scenario #2) +# --------------------------------------------------------------------------- + +@pytest.mark.skipif(not PARALLEL_AUTO_JSON.exists(), reason="synthetic_parallel_auto.json missing") +def test_branch_automation_zero_is_noop(tmp_path): + out = tmp_path / "p.json" + m = build_branch_automation_params( + PARALLEL_AUTO_JSON, automate_task_ids=["t_nc1", "t_nc2", "t_nc3"], + level=0, out_json_path=out, + ) + assert m["factor"] == 1.0 + src = json.loads(PARALLEL_AUTO_JSON.read_text()) + dst = json.loads(out.read_text()) + assert src["task_resource_distribution"] == dst["task_resource_distribution"] + + +@pytest.mark.skipif(not PARALLEL_AUTO_JSON.exists(), reason="synthetic_parallel_auto.json missing") +def test_branch_automation_shrinks_only_targets(tmp_path): + out = tmp_path / "p.json" + build_branch_automation_params( + PARALLEL_AUTO_JSON, automate_task_ids=["t_nc1", "t_nc2", "t_nc3"], + level=50, out_json_path=out, + ) + src = json.loads(PARALLEL_AUTO_JSON.read_text()) + dst = json.loads(out.read_text()) + # Non-critical tasks halved; critical and common tasks untouched. + for tid in ("t_nc1", "t_nc2", "t_nc3"): + assert _task_mean(dst, tid) == pytest.approx(_task_mean(src, tid) * 0.5) + for tid in ("t_crit", "t_reg", "t_dec"): + assert _task_mean(dst, tid) == _task_mean(src, tid) + + +@pytest.mark.skipif(not PARALLEL_AUTO_JSON.exists(), reason="synthetic_parallel_auto.json missing") +def test_branch_automation_floors_at_full_level(tmp_path): + out = tmp_path / "p.json" + build_branch_automation_params( + PARALLEL_AUTO_JSON, automate_task_ids=["t_nc1"], level=100, + floor_seconds=2.0, out_json_path=out, + ) + dst = json.loads(out.read_text()) + # The activity must still carry a strictly-positive (floored) duration so it + # stays in the trace -> ngd stays blind. + assert _task_mean(dst, "t_nc1") == 2.0 + + +def test_branch_automation_rejects_bad_args(tmp_path): + base = tmp_path / "base.json" + base.write_text(json.dumps({"task_resource_distribution": [ + {"task_id": "t_nc1", "resources": [ + {"distribution_name": "fix", "distribution_params": [{"value": 100.0}], "resource_id": "R1"}]}, + ]})) + with pytest.raises(ValueError): + build_branch_automation_params(base, automate_task_ids=[], level=10, out_json_path=tmp_path / "o.json") + with pytest.raises(ValueError): + build_branch_automation_params(base, automate_task_ids=["t_nc1"], level=-1, out_json_path=tmp_path / "o.json") + with pytest.raises(ValueError): + build_branch_automation_params(base, automate_task_ids=["t_nc1"], level=101, out_json_path=tmp_path / "o.json") + with pytest.raises(ValueError, match="not found"): + build_branch_automation_params(base, automate_task_ids=["nope"], level=10, out_json_path=tmp_path / "o.json") + + +# --------------------------------------------------------------------------- +# front_back_load: total-invariant duration redistribution (scenario #4) +# --------------------------------------------------------------------------- + +_CHAIN = ["t_step1", "t_step2", "t_step3", "t_step4", "t_step5"] + + +@pytest.mark.skipif(not LINEAR_CHAIN_JSON.exists(), reason="synthetic_linear_chain.json missing") +def test_front_back_load_zero_is_noop(tmp_path): + out = tmp_path / "p.json" + m = build_front_back_load_params( + LINEAR_CHAIN_JSON, chain_task_ids=_CHAIN, shift=0, out_json_path=out, + ) + assert all(f == 1.0 for f in m["factors"].values()) + src = json.loads(LINEAR_CHAIN_JSON.read_text()) + dst = json.loads(out.read_text()) + assert src["task_resource_distribution"] == dst["task_resource_distribution"] + + +@pytest.mark.skipif(not LINEAR_CHAIN_JSON.exists(), reason="synthetic_linear_chain.json missing") +@pytest.mark.parametrize("shift", [30, 60, -30, -60]) +def test_front_back_load_preserves_total(tmp_path, shift): + out = tmp_path / "p.json" + build_front_back_load_params( + LINEAR_CHAIN_JSON, chain_task_ids=_CHAIN, shift=shift, out_json_path=out, + ) + src = json.loads(LINEAR_CHAIN_JSON.read_text()) + dst = json.loads(out.read_text()) + # Per-case total duration (= cycle time, aggregate ρ) held exactly. + assert _chain_total_mean(dst, _CHAIN) == pytest.approx(_chain_total_mean(src, _CHAIN)) + + +@pytest.mark.skipif(not LINEAR_CHAIN_JSON.exists(), reason="synthetic_linear_chain.json missing") +def test_front_back_load_direction(tmp_path): + src = json.loads(LINEAR_CHAIN_JSON.read_text()) + front = tmp_path / "front.json" + back = tmp_path / "back.json" + build_front_back_load_params(LINEAR_CHAIN_JSON, chain_task_ids=_CHAIN, shift=60, out_json_path=front) + build_front_back_load_params(LINEAR_CHAIN_JSON, chain_task_ids=_CHAIN, shift=-60, out_json_path=back) + fd, bd = json.loads(front.read_text()), json.loads(back.read_text()) + # Front-load: early step longer than late step. Back-load: mirror image. + assert _task_mean(fd, "t_step1") > _task_mean(fd, "t_step5") + assert _task_mean(bd, "t_step1") < _task_mean(bd, "t_step5") + # Midpoint is the fixed pivot of the antisymmetric ramp. + assert _task_mean(fd, "t_step3") == pytest.approx(_task_mean(src, "t_step3")) + # Front(+s) and back(-s) are reflections about the chain midpoint. + assert _task_mean(fd, "t_step1") == pytest.approx(_task_mean(bd, "t_step5")) + + +def test_front_back_load_rejects_bad_args(tmp_path): + base = tmp_path / "base.json" + base.write_text(json.dumps({"task_resource_distribution": [ + {"task_id": "t_step1", "resources": [ + {"distribution_name": "fix", "distribution_params": [{"value": 100.0}], "resource_id": "R1"}]}, + {"task_id": "t_step2", "resources": [ + {"distribution_name": "fix", "distribution_params": [{"value": 100.0}], "resource_id": "R1"}]}, + ]})) + with pytest.raises(ValueError, match="weights stay positive"): + build_front_back_load_params(base, chain_task_ids=["t_step1", "t_step2"], shift=100, out_json_path=tmp_path / "o.json") + with pytest.raises(ValueError, match="at least two"): + build_front_back_load_params(base, chain_task_ids=["t_step1"], shift=10, out_json_path=tmp_path / "o.json") + with pytest.raises(ValueError, match="not found"): + build_front_back_load_params(base, chain_task_ids=["t_step1", "nope"], shift=10, out_json_path=tmp_path / "o.json") diff --git a/tests/evaluation/state_metrics/test_pipeline.py b/tests/evaluation/state_metrics/test_pipeline.py index ea6860c..7e17e81 100644 --- a/tests/evaluation/state_metrics/test_pipeline.py +++ b/tests/evaluation/state_metrics/test_pipeline.py @@ -14,6 +14,7 @@ _select_n_ongoing_cutoff, _write_prefix_csv, filter_to_cases_in_window, + drift_case_types, get_ongoing_case_ids, label_swap_case_types, merge_logs_at_ratio, @@ -487,3 +488,67 @@ def test_compute_utilization_rows_partial_busy(): assert by_role["Team A"]["utilization"] == pytest.approx(9000 / (2 * 14400)) # Team B never used → 0. assert by_role["Team B"]["utilization"] == 0.0 + + +# --------------------------------------------------------------------------- +# drift_case_types: temporal case-type drift (scenario #6) +# --------------------------------------------------------------------------- + +def _typed_timeline(n, rng_tags=None): + """n cases, one event each, arrival-ordered; alternating red/blue tags.""" + rows = [] + base = pd.Timestamp("2025-01-01", tz="UTC") + for i in range(n): + tag = "red" if i % 2 == 0 else "blue" + rows.append((f"c{i}", "A", base + pd.Timedelta(hours=i), + base + pd.Timedelta(hours=i + 1), "R1", tag)) + return _mklog_typed(rows) + + +def test_drift_zero_is_identity(): + log = _typed_timeline(10) + out = drift_case_types(log, strength=0.0, rng=np.random.default_rng(0)) + assert out.equals(log) + + +def test_drift_preserves_marginal(): + log = _typed_timeline(20) + for s in (0.25, 0.5, 0.75, 1.0): + out = drift_case_types(log, strength=s, rng=np.random.default_rng(1)) + assert (out["case_type"] == "red").sum() == 10 + assert (out["case_type"] == "blue").sum() == 10 + + +def test_drift_full_strength_is_time_sorted(): + log = _typed_timeline(20) + out = drift_case_types(log, strength=1.0, rng=np.random.default_rng(2)) + ordered = out.sort_values("start_time")["case_type"].tolist() + # Lexicographically-smaller value ("blue") lands on the earliest arrivals. + assert ordered == ["blue"] * 10 + ["red"] * 10 + + +def test_drift_leaves_event_stream_untouched(): + log = _typed_timeline(12) + out = drift_case_types(log, strength=1.0, rng=np.random.default_rng(3)) + for col in ("case_id", "activity", "start_time", "end_time", "resource"): + assert (out[col].values == log[col].values).all() + + +def test_drift_requires_case_type_column(): + log = _mklog([("c1", "A", "2025-01-01 10:00", "2025-01-01 11:00", "R1")]) + with pytest.raises(ValueError): + drift_case_types(log, strength=0.5, rng=np.random.default_rng(0)) + + +def test_drift_increases_temporal_sortedness_monotonically(): + # As strength rises, the first-half red fraction falls toward 0 (blue early). + log = _typed_timeline(40) + n = 40 + prev = None + for s in (0.0, 0.5, 1.0): + out = drift_case_types(log, strength=s, rng=np.random.default_rng(7)) + ordered = out.sort_values("start_time")["case_type"].tolist() + first_half_red = sum(t == "red" for t in ordered[: n // 2]) + if prev is not None: + assert first_half_red <= prev + prev = first_half_red diff --git a/tools/generate_linear_chain.py b/tools/generate_linear_chain.py new file mode 100644 index 0000000..3c5d665 --- /dev/null +++ b/tools/generate_linear_chain.py @@ -0,0 +1,171 @@ +"""Generate the synthetic *linear-chain* dataset (BPMN + Prosimos params). + +A purely sequential process of ``n_tasks`` comparable-duration activities on a +single resource pool: + + Start -> Step_1 -> Step_2 -> ... -> Step_N -> End + +No gateways, no branching: every case runs the same activity sequence with the +same per-task service distribution, so the baseline is symmetric and the only +thing a perturbation can move is *where in the case* duration mass sits. + +The state-metrics ``front_back_load`` perturbation +(``evaluation.state_metrics.perturb.build_front_back_load_params``) reweights +the per-task means by chain position while holding the per-case **total** +duration constant: front-loading makes early steps longer and late steps +shorter (back-loading is the mirror). Because the per-case total (cycle time), +the activity set, the bigrams, and the aggregate utilisation are all unchanged, +``cycle_time`` and ``ngd_n2`` are blind; only the time-weighted state metric +localises the moved duration mass — the timeline-localisation story. + +Run:: + + python -m tools.generate_linear_chain + python -m tools.generate_linear_chain --tasks 5 --out-dir samples/dev-samples +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from xml.sax.saxutils import escape + +BPMN_NS = "http://www.omg.org/spec/BPMN/20100524/MODEL" + + +def _task(tid: str, name: str, incoming: str, outgoing: str) -> str: + return ( + f'' + f"{incoming}" + f"{outgoing}" + f"" + ) + + +def _flow(fid: str, src: str, tgt: str) -> str: + return f'' + + +def build_model( + n_tasks: int, + *, + workers: int = 40, + arrival_mean: float = 90.0, + service: float = 180.0, +) -> tuple[str, dict, list[str]]: + """Return (bpmn_xml, prosimos_params, chain_task_ids). + + All ``n_tasks`` steps share the same ``service`` mean, so the unperturbed + chain is duration-symmetric — front/back-loading is a pure redistribution. + The pool is sized so contention is minimal (the win is timeline, not + throughput). + """ + if n_tasks < 2: + raise ValueError("n_tasks must be >= 2") + + nodes: list[str] = [] + flows: list[str] = [] + task_ids: list[str] = [] + + nodes.append('' + 'f_start') + prev_out = "f_start" + prev_src = "StartEvent_1" + for i in range(1, n_tasks + 1): + tid = f"t_step{i}" + task_ids.append(tid) + out_flow = "f_end" if i == n_tasks else f"f_step{i}" + nodes.append(_task(tid, f"Step_{i}", prev_out, out_flow)) + flows.append(_flow(prev_out, prev_src, tid)) + prev_out = out_flow + prev_src = tid + nodes.append('' + 'f_end') + flows.append(_flow("f_end", prev_src, "EndEvent_1")) + + process_body = "".join(nodes) + "".join(flows) + bpmn = ( + '' + f'' + '' + f"{process_body}" + "" + ) + + params = _build_params(task_ids, workers=workers, + arrival_mean=arrival_mean, service=service) + return bpmn, params, task_ids + + +def _build_params( + task_ids: list[str], *, workers: int, arrival_mean: float, service: float, +) -> dict: + cal_id = "cal_247" + periods = [{"from": "MONDAY", "to": "SUNDAY", + "beginTime": "00:00:00", "endTime": "23:59:59.999"}] + resource_ids = [f"Worker_{i}" for i in range(1, workers + 1)] + + def fix_service(rid: str) -> dict: + return { + "distribution_name": "fix", + "distribution_params": [{"value": service}], + "resource_id": rid, + } + + trd = [ + {"task_id": tid, "resources": [fix_service(r) for r in resource_ids]} + for tid in task_ids + ] + resource_profiles = [{ + "id": "Worker_profile", "name": "Worker", + "resource_list": [ + {"id": rid, "name": rid, "cost_per_hour": 0, "amount": 1, + "calendar": cal_id, "assignedTasks": list(task_ids)} + for rid in resource_ids + ], + }] + + return { + "task_resource_distribution": trd, + "resource_calendars": [{"id": cal_id, "name": "24/7", "time_periods": periods}], + "gateway_branching_probabilities": [], + "arrival_time_distribution": { + "distribution_name": "expon", + "distribution_params": [{"value": arrival_mean}, {"value": 0}, {"value": 3600}], + }, + "arrival_time_calendar": periods, + "resource_profiles": resource_profiles, + "event_distribution": [], + "batch_processing": [], + "case_attributes": [], + "prioritisation_rules": [], + } + + +def main() -> None: + ap = argparse.ArgumentParser(description="Generate the linear-chain synthetic dataset") + ap.add_argument("--tasks", type=int, default=5) + ap.add_argument("--workers", type=int, default=40) + ap.add_argument("--arrival-mean", type=float, default=90.0) + ap.add_argument("--service", type=float, default=180.0) + ap.add_argument("--out-dir", type=Path, + default=Path(__file__).resolve().parents[1] / "samples" / "dev-samples") + ap.add_argument("--name", default="synthetic_linear_chain") + args = ap.parse_args() + + bpmn, params, ids = build_model(args.tasks, workers=args.workers, + arrival_mean=args.arrival_mean, service=args.service) + args.out_dir.mkdir(parents=True, exist_ok=True) + bpmn_path = args.out_dir / f"{args.name}.bpmn" + json_path = args.out_dir / f"{args.name}.json" + bpmn_path.write_text(bpmn, encoding="utf-8") + json_path.write_text(json.dumps(params, indent=2), encoding="utf-8") + print(f"wrote {bpmn_path}") + print(f"wrote {json_path}") + print(f" chain tasks (ordered): {ids}") + + +if __name__ == "__main__": + main() diff --git a/tools/generate_parallel_auto.py b/tools/generate_parallel_auto.py new file mode 100644 index 0000000..2d4fd12 --- /dev/null +++ b/tools/generate_parallel_auto.py @@ -0,0 +1,263 @@ +"""Generate the synthetic *parallel-automation* dataset (BPMN + Prosimos params). + +The process has one AND (parallel) block with a *critical* and a *non-critical* +branch running concurrently: + + Start -> Register + -> AND-split + -> Critical (long; sets the cycle time) + -> NonCritical_1 -> ... -> NonCritical_M (short chain) + -> AND-join + -> Decide -> End + +The critical branch's duration strictly dominates the non-critical chain +(``D_crit`` >> ``Σ D_nc``) and the two branches run on SEPARATE resource pools. +Consequently: + +* the per-case cycle time is set by the critical branch, so shrinking the + non-critical work leaves cycle time unchanged; +* automating the non-critical branch does not touch the critical pool, so the + critical resource utilisation is unchanged. + +The state-metrics ``parallel_auto`` perturbation +(``evaluation.state_metrics.perturb.build_branch_automation_params``) scales the +non-critical task durations toward a small floor. Because the non-critical +activities still appear once per case (the floor keeps them in the trace) but +occupy the active-instance set for progressively less time, the per-instant +active multiset shifts while: + +* ``cycle_time`` stays blind (critical path unchanged), and +* ``ngd_n2`` stays blind (same activities, same bigrams, same per-case counts). + +Only the time-weighted state metric sees the non-critical branch vanishing from +the concurrent active set. This is Marlon's parallel-branch-automation example. + +Run:: + + python -m tools.generate_parallel_auto + python -m tools.generate_parallel_auto --nc-tasks 3 --out-dir samples/dev-samples +""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path +from xml.sax.saxutils import escape + +BPMN_NS = "http://www.omg.org/spec/BPMN/20100524/MODEL" + + +def _task(tid: str, name: str, incoming: str, outgoing: str) -> str: + return ( + f'' + f"{incoming}" + f"{outgoing}" + f"" + ) + + +def _parallel(gid: str, name: str, incoming: list[str], outgoing: list[str]) -> str: + inc = "".join(f"{f}" for f in incoming) + out = "".join(f"{f}" for f in outgoing) + return f'{inc}{out}' + + +def _flow(fid: str, src: str, tgt: str) -> str: + return f'' + + +def build_model( + n_nc_tasks: int, + *, + common_workers: int = 12, + critical_workers: int = 40, + nc_workers: int = 40, + arrival_mean: float = 30.0, + common_service: float = 60.0, + critical_service: float = 600.0, + nc_service: float = 120.0, +) -> tuple[str, dict, list[str]]: + """Return (bpmn_xml, prosimos_params, nc_task_ids). + + ``critical_service`` must dominate ``n_nc_tasks * nc_service`` so the + critical branch sets the cycle time; the defaults give 600 vs 360. + Pools are sized generously so contention is minimal and the win is a pure + composition/timeline effect rather than a throughput effect. + """ + if n_nc_tasks < 1: + raise ValueError("n_nc_tasks must be >= 1") + nc_total = n_nc_tasks * nc_service + if critical_service <= nc_total: + raise ValueError( + f"critical_service ({critical_service}) must exceed the non-critical " + f"chain total ({nc_total}) so the critical branch sets cycle time" + ) + + nodes: list[str] = [] + flows: list[str] = [] + + nodes.append('' + 'f_start') + nodes.append(_task("t_reg", "Register", "f_start", "f_reg_split")) + flows.append(_flow("f_start", "StartEvent_1", "t_reg")) + + # AND-split: one incoming (from Register), two outgoing (critical / nc). + f_split_crit = "f_split_crit" + f_split_nc = "f_split_nc0" + flows.append(_flow("f_reg_split", "t_reg", "g_split")) + nodes.append(_parallel("g_split", "AND-split", ["f_reg_split"], + [f_split_crit, f_split_nc])) + + # Critical branch: single long task into the join. + f_crit_join = "f_crit_join" + nodes.append(_task("t_crit", "Critical", f_split_crit, f_crit_join)) + flows.append(_flow(f_split_crit, "g_split", "t_crit")) + + # Non-critical branch: a chain of short tasks into the join. + nc_task_ids: list[str] = [] + prev_out = f_split_nc + prev_src = "g_split" + for i in range(1, n_nc_tasks + 1): + tid = f"t_nc{i}" + nc_task_ids.append(tid) + is_last = i == n_nc_tasks + out_flow = "f_nc_join" if is_last else f"f_nc{i}" + nodes.append(_task(tid, f"NonCritical_{i}", prev_out, out_flow)) + flows.append(_flow(prev_out, prev_src, tid)) + prev_out = out_flow + prev_src = tid + + # AND-join: two incoming (critical / nc), one outgoing (to Decide). + nodes.append(_parallel("g_join", "AND-join", [f_crit_join, "f_nc_join"], + ["f_join_dec"])) + flows.append(_flow(f_crit_join, "t_crit", "g_join")) + flows.append(_flow("f_nc_join", prev_src, "g_join")) + + nodes.append(_task("t_dec", "Decide", "f_join_dec", "f_dec_end")) + flows.append(_flow("f_join_dec", "g_join", "t_dec")) + nodes.append('' + 'f_dec_end') + flows.append(_flow("f_dec_end", "t_dec", "EndEvent_1")) + + process_body = "".join(nodes) + "".join(flows) + bpmn = ( + '' + f'' + '' + f"{process_body}" + "" + ) + + params = _build_params( + nc_task_ids, + common_workers=common_workers, critical_workers=critical_workers, + nc_workers=nc_workers, arrival_mean=arrival_mean, + common_service=common_service, critical_service=critical_service, + nc_service=nc_service, + ) + return bpmn, params, nc_task_ids + + +def _pool(name: str, prefix: str, n: int, cal_id: str, tasks: list[str]) -> dict: + return { + "id": f"{name}_profile", + "name": name, + "resource_list": [ + {"id": f"{prefix}_{i}", "name": f"{prefix}_{i}", "cost_per_hour": 0, + "amount": 1, "calendar": cal_id, "assignedTasks": list(tasks)} + for i in range(1, n + 1) + ], + } + + +def _build_params( + nc_task_ids: list[str], + *, + common_workers: int, + critical_workers: int, + nc_workers: int, + arrival_mean: float, + common_service: float, + critical_service: float, + nc_service: float, +) -> dict: + cal_id = "cal_247" + periods = [{"from": "MONDAY", "to": "SUNDAY", + "beginTime": "00:00:00", "endTime": "23:59:59.999"}] + + common_tasks = ["t_reg", "t_dec"] + critical_tasks = ["t_crit"] + + common_pool = _pool("Common", "Common", common_workers, cal_id, common_tasks) + critical_pool = _pool("Critical", "Critical", critical_workers, cal_id, critical_tasks) + nc_pool = _pool("NonCritical", "NonCritical", nc_workers, cal_id, nc_task_ids) + + def fix(rid: str, service: float) -> dict: + return { + "distribution_name": "fix", + "distribution_params": [{"value": service}], + "resource_id": rid, + } + + trd: list[dict] = [] + for tid in common_tasks: + trd.append({"task_id": tid, + "resources": [fix(r["id"], common_service) + for r in common_pool["resource_list"]]}) + for tid in critical_tasks: + trd.append({"task_id": tid, + "resources": [fix(r["id"], critical_service) + for r in critical_pool["resource_list"]]}) + for tid in nc_task_ids: + trd.append({"task_id": tid, + "resources": [fix(r["id"], nc_service) + for r in nc_pool["resource_list"]]}) + + return { + "task_resource_distribution": trd, + "resource_calendars": [{"id": cal_id, "name": "24/7", "time_periods": periods}], + # Parallel (AND) gateways take all branches -> no branching probabilities. + "gateway_branching_probabilities": [], + "arrival_time_distribution": { + "distribution_name": "expon", + "distribution_params": [{"value": arrival_mean}, {"value": 0}, {"value": 3600}], + }, + "arrival_time_calendar": periods, + "resource_profiles": [common_pool, critical_pool, nc_pool], + "event_distribution": [], + "batch_processing": [], + "case_attributes": [], + "prioritisation_rules": [], + } + + +def main() -> None: + ap = argparse.ArgumentParser(description="Generate the parallel-automation synthetic dataset") + ap.add_argument("--nc-tasks", type=int, default=3, + help="number of tasks in the non-critical chain") + ap.add_argument("--arrival-mean", type=float, default=30.0) + ap.add_argument("--critical-service", type=float, default=600.0) + ap.add_argument("--nc-service", type=float, default=120.0) + ap.add_argument("--out-dir", type=Path, + default=Path(__file__).resolve().parents[1] / "samples" / "dev-samples") + ap.add_argument("--name", default="synthetic_parallel_auto") + args = ap.parse_args() + + bpmn, params, nc_ids = build_model( + args.nc_tasks, arrival_mean=args.arrival_mean, + critical_service=args.critical_service, nc_service=args.nc_service, + ) + args.out_dir.mkdir(parents=True, exist_ok=True) + bpmn_path = args.out_dir / f"{args.name}.bpmn" + json_path = args.out_dir / f"{args.name}.json" + bpmn_path.write_text(bpmn, encoding="utf-8") + json_path.write_text(json.dumps(params, indent=2), encoding="utf-8") + print(f"wrote {bpmn_path}") + print(f"wrote {json_path}") + print(f" non-critical tasks (automation targets): {nc_ids}") + + +if __name__ == "__main__": + main()