Scenario Script
Write Python that modifies or analyses an assembled scenario.
When to use this¶
- Build what-if variants of a scenario — scale demand, drop a location, shift costs
- Enrich a scenario with computed data
- Bridge between analyses, for example feeding Center of Gravity results into Network Optimization
For custom logic on raw rows, before a scenario exists, the core's Script node is the one to reach for: it runs on lists of row dicts, this one runs on scenario data.
What you need¶
- A scenario wired into the Scenario Data input, from Create Scenario, Load Scenario, or any node that produces scenario data
- Optionally, a table wired into the Auxiliary Table input — a cadence calendar, exchange rates, an external rate table
Parameters¶
| Setting | Options | What it is |
|---|---|---|
| Python code | your main(scenario_data) function |
Required. The Python this node runs. |
Return a dictionary with at least one scenario table. Every scenario table you
return must be a list of row dictionaries, and an explicitly empty table stays
empty. Return every table and key you were given, so that its tables, metadata
and overrides go on with it: a scenario that leaves one out stops the run with
an error naming it. To empty a table, set it to [].
For rows or summary metrics, use Scenario to Table. Scenario Script rejects lists, scalar metrics dictionaries and raw values rather than guessing a format.
What this node runs, and where¶
This node carries code its author wrote, so the whole graph runs in the isolated runner rather than in the API process. That is what makes it safe to offer at all, and it is declared on the node type rather than inferred.
The code may import only a fixed list of standard-library modules, shown under
Allowed imports in the node's panel. Any other import is refused before the
node runs, and the refusal lists the modules that are allowed. No pandas, no
numpy. The code may also use only the names it defines, imports or takes from
Python's builtins; any other name is refused the same way.
What you can read from scenario_data¶
35 tables, and every one of them is there even when it has no rows. A load materialises the empty ones too, so scenario_data['supply_state'] is [] for a scenario with no supply state rather than a KeyError. Each row is a dict with at least the keys below, in this order; a column the data carried beyond them follows under its own name. A scenario can also carry tables beyond these — a record type the mappers do not declare is stored under its own name, and a key you add to the scenario dict with a list of rows is stored as a table of that name — so read scenario_data by key rather than assuming the list below is all of it.
| Table | Keys on each row |
|---|---|
bom |
bom_name, output_product, component_product, quantity_per_output, output_unit, component_unit |
cog-results |
name, latitude, longitude, result_type, weighted_distance, source_location_name, total_weighted_distance, iterations, converged, location_mode |
consolidation-lanes |
source, target, baseline_transports, consolidated_transports, transports_saved |
consolidation-shipments |
source_row, shipment, transport, transport_row, disposition, reason, source, target, date, product, transport_mode, load, load_unit, quantity, quantity_unit, transport_date, transport_load, transport_load_unit |
consolidation-summary |
baseline_transports, consolidated_transports, transports_saved, transports_saved_share, excluded_rows |
consolidation-weekly |
week_start, baseline_transports, consolidated_transports, transports_saved |
consolidation_state |
source_row, shipment, transport, transport_row, disposition, reason, source, target, date, product, transport_mode, load, load_unit, quantity, quantity_unit |
cost-to-serve |
demand_location, product, cost_type, cost, allocation_type, system_location, system_reason, source_product, source_location, source_target, source_transport_mode, source_bom_name |
demand-forecast |
location, product, date, forecast, forecast_low, forecast_high, model, unit |
demand_history |
location, product, date, quantity, unit |
demand_policy |
location, product, requested_quantity, quantity_unit, unit_revenue |
demand_state |
location, product, fulfilled_quantity, quantity_unit, realized_revenue |
flows |
source, target, product, quantity, quantity_unit, cost, emissions, emissions_unit, transport_mode, date, unit_cost, load, load_unit, reference |
handling_policy |
location, product, unit_handling_cost, quantity_unit |
handling_state |
location, product, realized_cost |
inventory_policy |
location, product, unit_carrying_cost, average_days_of_inventory, unit_carrying_cost_period, quantity_unit |
inventory_state |
location, product, average_inventory, inventory_unit, realized_carrying_cost |
item-segments |
product, location, class, reason, detail, periods, nonzero_periods, mean, standard_deviation, cv, adi, total_quantity, unit, first_period, last_period |
lanes |
source, target, product, unit_cost, capacity, lead_time, emissions_per_unit, emissions_unit, transport_mode, min_quantity, quantity_unit |
location_policy |
location, mode, fixed_operating_cost |
location_state |
location, is_open, total_fixed_cost |
locations |
id, address, latitude, longitude, type, tags, val |
objective-terms |
term, value, unit, weight, contribution |
optimization-details |
objective_type, objective_sense, profit_weight, emissions_weight, objective_value, status, proven_optimal, best_bound, mip_gap, solver, problem_class, solve_seconds, presolve_seconds, time_budget_seconds, precision_target_pct, simplex_iterations, branch_and_bound_nodes, variables, integer_variables, constraints, nonzeros, presolved_variables, presolved_constraints, presolved_nonzeros, matrix_coefficient_min, matrix_coefficient_max, cost_coefficient_min, cost_coefficient_max, bound_min, bound_max, rhs_min, rhs_max, max_constraint_violation, debug_mode, debug_binding_step |
penalties |
origin, step, kind, location, product, source, target, mode, constraint_params, bound, reached, missed_by |
production_policy |
location, bom_name, unit_production_cost, capacity, quantity_unit, emissions_per_unit, emissions_unit |
production_state |
location, bom_name, produced_quantity, quantity_unit, realized_cost, realized_emissions, emissions_unit |
products |
product, product_group, description, tags |
rules |
rule, kind, scope, member, source, target, location, product, mode, sense, bound, reached, slack, slack_share, missed_by, status, unit, when_used, constraint_params |
scenario-unit-factors |
table, key, unit, factor |
scenario-units |
quantity_unit |
segmentation-method |
measure, measure_definition, group_by, period_grain, window_start, window_end, periods_in_window, gap_treatment, x_max_cv, y_max_cv, class_rule, min_periods, min_nonzero_periods, reported_but_not_classified_on, seasonality_removed, outliers_treated, rows_read, rows_in_window, rows_outside_window, items_seen |
supply_policy |
location, product, capacity, quantity_unit, unit_supply_cost |
supply_state |
location, product, used_quantity, quantity_unit, realized_cost, realized_emissions, emissions_unit |
unit_conversions |
product, from_quantity, from_unit, to_quantity, to_unit |
Keys beginning with an underscore are another node's state riding along: _origin_scenario_id and _scenario_meta from a load, _scenario_id, _cog_result from a siting run, _optimization_overrides from an add_constraint or add_costs. Read them; do not delete them, and do not treat them as tables.
Return every key you were given. Change scenario_data and return it, or build a new dictionary as {**scenario_data, "demand_policy": new_rows}. A returned scenario that leaves out any table or underscore key it was given stops the run with an error naming each one; to empty a table, set it to [].
What the run writes back¶
Return the scenario and its tables are re-persisted as they stand. Three keys are read differently: they name an enrichment the run produced, and the final output's value for one of them is written into the table beside it.
| Key on the run's output | Table it lands in |
|---|---|
cost_to_serve |
cost-to-serve |
demand_forecast |
demand-forecast |
inventory_state |
inventory_state |
Two of those are spelled one way when read and another when written, and the difference is the point: cost-to-serve is a stored table, cost_to_serve is the key an enrichment arrives under. Absent is not empty — a key your output does not carry leaves the stored table exactly as it was, which is what stops one run erasing what an earlier one computed.
Examples¶
Scale demand — scenario output¶
def main(scenario_data):
"""Increase all requested demand by 10%."""
for d in scenario_data.get("demand_policy", []):
d["requested_quantity"] = float(d.get("requested_quantity", 0)) * 1.1
return scenario_data
The auxiliary table input¶
Wire a second upstream — typically Import Data — into Auxiliary Table. Its rows arrive as a second argument beside the scenario:
def main(scenario_data, table_data):
"""table_data is a list of row dicts from the upstream node, or None
when the auxiliary port is not wired."""
cadence = {
(r["product"], r["destination"]): int(r["weeks_between_deliveries"])
for r in (table_data or [])
}
# ... use cadence to adjust scenario_data ...
return scenario_data
The one-argument form still works: the Auxiliary Table input is optional.
Workflow wiring¶
Modify a scenario, then save it:
flowchart LR
n1["Load Scenario"] --> n2["Scenario Script (into Scenario Data)"]
n2 --> n3["Create Scenario"]
Modify, then optimize:
flowchart LR
n1["Load Scenario"] --> n2["Scenario Script (into Scenario Data)"]
n2 --> n3["Network Optimization"]
n3 --> n4["Create Scenario"]
Analyse a freshly built scenario:
flowchart LR
n1["Import Data (locations.csv)"] --> n2["Map Locations"]
n3["Create Scenario"] --> n4["Scenario Script (into Scenario Data)"]
n5["Import Data (demand.csv)"] --> n6["Map Demand Policy"]
n2 --> n3
n6 --> n3
n7["Import Data (products.csv)"] --> n8["Map Products"]
n8 --> n3
With an auxiliary table — the scenario goes into Scenario Data, the table into Auxiliary Table:
flowchart LR
n2["Scenario Script"] --> n3["Create Scenario"]
n1["Load Scenario"] --> n2
n4["Import Data (cadence)"] --> n2
Common mistakes¶
- Returning rows or a metrics dictionary. Use Scenario to Table and return a list of row dictionaries.
- Returning only part of the scenario. The run stops and names what is missing, such as Center of Gravity's sites or XYZ Segmentation's classes. Modify and return the full input, or start the new dictionary from
{**scenario_data}. - Using this node on raw rows. Use the core Script node before a scenario exists.
- Editing a table that is still empty. Results tables such as
flowshold no rows until an optimization has run upstream, so scaling them changes nothing. When the scenario this node returns is identical to the one it received, the run reports a Script changed nothing warning on the node.
Messages this node can show¶
A run gives this node 10 minutes. A node that takes longer stops with Time limit reached.
Like every node, it can also stop on the checks before a run and the run's limits.