[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_price_band_tight
Scenario ID: retail_f2_price_band_tight_v3

[BUSINESS DESCRIPTION]
Business narrative:
The retailer operates a category with a value-oriented basic product and a premium product that are sold under tight price bands and margin trade-offs. Premium carries higher effective margin and stricter implicit service expectations, while basic is cheaper but more price sensitive. When Basic inventory is insufficient, Basic's demand can be served by Premium (upward substitution); the reverse does not occur. Unmet demand after this limited upgrade behavior is lost and penalized.

Structure cues:
- The physical and inventory structure follows the same pattern as in retail_f1_base: single-echelon, local demand, local inventory, shared storage, and no transshipment.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- The substitution graph contains a single directed edge [Basic, Premium]. This means: when Basic inventory is insufficient, Premium inventory can serve Basic's demand. S[Basic, Premium] represents units of Basic's demand fulfilled by Premium.
- Lost sales penalties and purchasing or production costs in the JSON make premium more valuable to protect and basic cheaper but more elastic. This asymmetry is expressed through cost parameters, not through any special-case constraints.
- Inventory remains non-negative; any demand that cannot be satisfied even after allowing the basic-to-premium substitution is recorded as lost sales.
- There is no transshipment, lead times are effectively zero, and labor capacity is non-binding.
- The objective is to minimize total cost including purchasing, holding, waste, and lost sales while respecting the one-way substitution rule and storage capacity limits.

[DATA SCHEMA]
{
  "name": str,                          # scenario identifier
  "periods": int,                       # number of time periods
  "products": [str, ...],               # list of product IDs
  "locations": [str, ...],              # list of location IDs

  "shelf_life": {p: int},               # shelf life in periods per product
  "lead_time": {p: int},                # order lead time per product (0 = same-period arrival)

  "demand_curve": {p: [float, ...]},    # demand per product per period (0-indexed list)
  "demand_share": {l: float},           # fraction of total demand at each location

  "production_cap": {p: [float, ...]},  # max production per product per period (0-indexed list)
  "cold_capacity": {l: float},          # storage capacity per location
  "cold_usage": {p: float},             # storage units per unit of product

  "labor_cap": {l: [float, ...]},       # labor hours per location per period (0-indexed list)
  "labor_usage": {p: float},            # labor hours per unit sold
  "return_rate": {p: float},            # fraction of sales returned next period

  "costs": {
    "purchasing": {p: float},           # cost per unit ordered
    "inventory": {p: float},            # holding cost per unit per period
    "waste": {p: float},                # cost per unit expired
    "lost_sales": {p: float},           # penalty per unit of unmet demand
    "fixed_order": float,               # fixed cost per order placed
    "transshipment": float              # cost per unit transshipped
  },

  "constraints": {
    "moq": float,                       # minimum order quantity (0 = no MOQ)
    "pack_size": int,                   # order must be multiple of this (1 = no constraint)
    "budget_per_period": float|null,    # max purchasing cost per period
    "waste_limit_pct": float|null       # max waste as fraction of total demand
  },

  "network": {
    "sub_edges": [[p_from, p_to], ...], # substitution: p_from's demand can be served by p_to
    "trans_edges": [[l_from, l_to], ...]# transshipment: can ship from l_from to l_to
  }
}

[DATA ACCESS]
- The variable `data` is pre-loaded. Do NOT use file I/O.
- Lists are 0-indexed (period t in model uses index [t-1] in data arrays)

CRITICAL - Network edges require tuple conversion for Gurobi:
  sub_edges = [tuple(e) for e in data.get('network', {}).get('sub_edges', [])]
  trans_edges = [tuple(e) for e in data.get('network', {}).get('trans_edges', [])]

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that models and solves this optimization problem.

[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_price_band_tight
Scenario ID: retail_f2_price_band_tight_v3

[BUSINESS DESCRIPTION]
Business narrative:
The retailer operates a category with a value-oriented basic product and a premium product that are sold under tight price bands and margin trade-offs. Premium carries higher effective margin and stricter implicit service expectations, while basic is cheaper but more price sensitive. When Basic inventory is insufficient, Basic's demand can be served by Premium (upward substitution); the reverse does not occur. Unmet demand after this limited upgrade behavior is lost and penalized.

Structure cues:
- The physical and inventory structure follows the same pattern as in retail_f1_base: single-echelon, local demand, local inventory, shared storage, and no transshipment.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- The substitution graph contains a single directed edge [Basic, Premium]. This means: when Basic inventory is insufficient, Premium inventory can serve Basic's demand. S[Basic, Premium] represents units of Basic's demand fulfilled by Premium.
- Lost sales penalties and purchasing or production costs in the JSON make premium more valuable to protect and basic cheaper but more elastic. This asymmetry is expressed through cost parameters, not through any special-case constraints.
- Inventory remains non-negative; any demand that cannot be satisfied even after allowing the basic-to-premium substitution is recorded as lost sales.
- There is no transshipment, lead times are effectively zero, and labor capacity is non-binding.
- The objective is to minimize total cost including purchasing, holding, waste, and lost sales while respecting the one-way substitution rule and storage capacity limits.

[DATA]
The following JSON contains all instance data. Parse it directly in your code.

```json
{
  "name": "retail_f2_price_band_tight_v3",
  "description": "Standard seasonal retail scenario.",
  "periods": 20,
  "products": [
    "SKU_Basic",
    "SKU_Premium",
    "SKU_ShortLife"
  ],
  "locations": [
    "DC1",
    "DC2",
    "DC3",
    "DC4",
    "DC5"
  ],
  "shelf_life": {
    "SKU_Basic": 10,
    "SKU_Premium": 8,
    "SKU_ShortLife": 4
  },
  "lead_time": {
    "SKU_Basic": 0,
    "SKU_Premium": 0,
    "SKU_ShortLife": 0
  },
  "cold_capacity": {
    "DC1": 3426.851027013356,
    "DC2": 4014.3338689418874,
    "DC3": 2845.2151192755423,
    "DC4": 2932.70194359208,
    "DC5": 2291.9453290268225
  },
  "cold_usage": {
    "SKU_Basic": 1.0,
    "SKU_Premium": 3.0,
    "SKU_ShortLife": 1.2
  },
  "production_cap": {
    "SKU_Basic": [
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800
    ],
    "SKU_Premium": [
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400
    ],
    "SKU_ShortLife": [
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500
    ]
  },
  "labor_cap": {
    "DC1": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC2": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC3": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC4": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC5": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ]
  },
  "labor_usage": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "return_rate": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "demand_curve": {
    "SKU_Basic": [
      304,
      290,
      361,
      363,
      493,
      483,
      660,
      998,
      1122,
      1147,
      1224,
      1151,
      1115,
      859,
      696,
      610,
      386,
      344,
      301,
      328
    ],
    "SKU_Premium": [
      148,
      141,
      175,
      169,
      216,
      236,
      345,
      475,
      600,
      613,
      716,
      702,
      477,
      425,
      402,
      288,
      211,
      191,
      164,
      161
    ],
    "SKU_ShortLife": [
      135,
      128,
      132,
      139,
      148,
      189,
      295,
      380,
      420,
      508,
      520,
      445,
      436,
      376,
      273,
      216,
      183,
      167,
      150,
      106
    ]
  },
  "demand_share": {
    "DC1": 0.25,
    "DC2": 0.2,
    "DC3": 0.2,
    "DC4": 0.2,
    "DC5": 0.15
  },
  "costs": {
    "lost_sales": {
      "SKU_Basic": 50.0,
      "SKU_Premium": 160.0,
      "SKU_ShortLife": 40.0
    },
    "inventory": {
      "SKU_Basic": 1.0,
      "SKU_Premium": 1.5,
      "SKU_ShortLife": 1.0
    },
    "waste": {
      "SKU_Basic": 2.0,
      "SKU_Premium": 3.0,
      "SKU_ShortLife": 2.0
    },
    "fixed_order": 0.0,
    "transshipment": 0.5,
    "purchasing": {
      "SKU_Basic": 11.0,
      "SKU_Premium": 16.0,
      "SKU_ShortLife": 15.0
    }
  },
  "constraints": {
    "moq": 0,
    "pack_size": 1,
    "budget_per_period": null,
    "waste_limit_pct": null
  },
  "network": {
    "sub_edges": [
      [
        "SKU_Basic",
        "SKU_Premium"
      ]
    ],
    "trans_edges": []
  }
}
```

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB; import json
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that:
1. Parses the JSON data above (use json.loads on the string)
2. Models and solves the optimization problem
3. Prints status and objective value

Standard seasonal retail scenario.


[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_price_band_tight
Scenario ID: retail_f2_price_band_tight_v4

[BUSINESS DESCRIPTION]
Business narrative:
The retailer operates a category with a value-oriented basic product and a premium product that are sold under tight price bands and margin trade-offs. Premium carries higher effective margin and stricter implicit service expectations, while basic is cheaper but more price sensitive. When Basic inventory is insufficient, Basic's demand can be served by Premium (upward substitution); the reverse does not occur. Unmet demand after this limited upgrade behavior is lost and penalized.

Structure cues:
- The physical and inventory structure follows the same pattern as in retail_f1_base: single-echelon, local demand, local inventory, shared storage, and no transshipment.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- The substitution graph contains a single directed edge [Basic, Premium]. This means: when Basic inventory is insufficient, Premium inventory can serve Basic's demand. S[Basic, Premium] represents units of Basic's demand fulfilled by Premium.
- Lost sales penalties and purchasing or production costs in the JSON make premium more valuable to protect and basic cheaper but more elastic. This asymmetry is expressed through cost parameters, not through any special-case constraints.
- Inventory remains non-negative; any demand that cannot be satisfied even after allowing the basic-to-premium substitution is recorded as lost sales.
- There is no transshipment, lead times are effectively zero, and labor capacity is non-binding.
- The objective is to minimize total cost including purchasing, holding, waste, and lost sales while respecting the one-way substitution rule and storage capacity limits.

[DATA SCHEMA]
{
  "name": str,                          # scenario identifier
  "periods": int,                       # number of time periods
  "products": [str, ...],               # list of product IDs
  "locations": [str, ...],              # list of location IDs

  "shelf_life": {p: int},               # shelf life in periods per product
  "lead_time": {p: int},                # order lead time per product (0 = same-period arrival)

  "demand_curve": {p: [float, ...]},    # demand per product per period (0-indexed list)
  "demand_share": {l: float},           # fraction of total demand at each location

  "production_cap": {p: [float, ...]},  # max production per product per period (0-indexed list)
  "cold_capacity": {l: float},          # storage capacity per location
  "cold_usage": {p: float},             # storage units per unit of product

  "labor_cap": {l: [float, ...]},       # labor hours per location per period (0-indexed list)
  "labor_usage": {p: float},            # labor hours per unit sold
  "return_rate": {p: float},            # fraction of sales returned next period

  "costs": {
    "purchasing": {p: float},           # cost per unit ordered
    "inventory": {p: float},            # holding cost per unit per period
    "waste": {p: float},                # cost per unit expired
    "lost_sales": {p: float},           # penalty per unit of unmet demand
    "fixed_order": float,               # fixed cost per order placed
    "transshipment": float              # cost per unit transshipped
  },

  "constraints": {
    "moq": float,                       # minimum order quantity (0 = no MOQ)
    "pack_size": int,                   # order must be multiple of this (1 = no constraint)
    "budget_per_period": float|null,    # max purchasing cost per period
    "waste_limit_pct": float|null       # max waste as fraction of total demand
  },

  "network": {
    "sub_edges": [[p_from, p_to], ...], # substitution: p_from's demand can be served by p_to
    "trans_edges": [[l_from, l_to], ...]# transshipment: can ship from l_from to l_to
  }
}

[DATA ACCESS]
- The variable `data` is pre-loaded. Do NOT use file I/O.
- Lists are 0-indexed (period t in model uses index [t-1] in data arrays)

CRITICAL - Network edges require tuple conversion for Gurobi:
  sub_edges = [tuple(e) for e in data.get('network', {}).get('sub_edges', [])]
  trans_edges = [tuple(e) for e in data.get('network', {}).get('trans_edges', [])]

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that models and solves this optimization problem.

[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_price_band_tight
Scenario ID: retail_f2_price_band_tight_v4

[BUSINESS DESCRIPTION]
Business narrative:
The retailer operates a category with a value-oriented basic product and a premium product that are sold under tight price bands and margin trade-offs. Premium carries higher effective margin and stricter implicit service expectations, while basic is cheaper but more price sensitive. When Basic inventory is insufficient, Basic's demand can be served by Premium (upward substitution); the reverse does not occur. Unmet demand after this limited upgrade behavior is lost and penalized.

Structure cues:
- The physical and inventory structure follows the same pattern as in retail_f1_base: single-echelon, local demand, local inventory, shared storage, and no transshipment.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- The substitution graph contains a single directed edge [Basic, Premium]. This means: when Basic inventory is insufficient, Premium inventory can serve Basic's demand. S[Basic, Premium] represents units of Basic's demand fulfilled by Premium.
- Lost sales penalties and purchasing or production costs in the JSON make premium more valuable to protect and basic cheaper but more elastic. This asymmetry is expressed through cost parameters, not through any special-case constraints.
- Inventory remains non-negative; any demand that cannot be satisfied even after allowing the basic-to-premium substitution is recorded as lost sales.
- There is no transshipment, lead times are effectively zero, and labor capacity is non-binding.
- The objective is to minimize total cost including purchasing, holding, waste, and lost sales while respecting the one-way substitution rule and storage capacity limits.

[DATA]
The following JSON contains all instance data. Parse it directly in your code.

```json
{
  "name": "retail_f2_price_band_tight_v4",
  "description": "Standard seasonal retail scenario.",
  "periods": 20,
  "products": [
    "SKU_Basic",
    "SKU_Premium",
    "SKU_ShortLife"
  ],
  "locations": [
    "DC1",
    "DC2",
    "DC3",
    "DC4",
    "DC5"
  ],
  "shelf_life": {
    "SKU_Basic": 10,
    "SKU_Premium": 8,
    "SKU_ShortLife": 4
  },
  "lead_time": {
    "SKU_Basic": 0,
    "SKU_Premium": 0,
    "SKU_ShortLife": 0
  },
  "cold_capacity": {
    "DC1": 3427.627149702561,
    "DC2": 3132.062723953799,
    "DC3": 2714.118544653094,
    "DC4": 2672.4786538885255,
    "DC5": 2561.026704650092
  },
  "cold_usage": {
    "SKU_Basic": 1.0,
    "SKU_Premium": 3.0,
    "SKU_ShortLife": 1.2
  },
  "production_cap": {
    "SKU_Basic": [
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800
    ],
    "SKU_Premium": [
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400
    ],
    "SKU_ShortLife": [
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500
    ]
  },
  "labor_cap": {
    "DC1": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC2": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC3": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC4": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC5": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ]
  },
  "labor_usage": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "return_rate": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "demand_curve": {
    "SKU_Basic": [
      268,
      313,
      345,
      390,
      442,
      479,
      785,
      905,
      1015,
      1418,
      1200,
      1223,
      1032,
      835,
      733,
      542,
      378,
      393,
      332,
      298
    ],
    "SKU_Premium": [
      130,
      163,
      173,
      156,
      210,
      304,
      341,
      473,
      564,
      547,
      575,
      563,
      627,
      514,
      332,
      304,
      203,
      158,
      165,
      164
    ],
    "SKU_ShortLife": [
      124,
      126,
      148,
      156,
      193,
      209,
      319,
      336,
      492,
      554,
      555,
      527,
      403,
      310,
      307,
      196,
      190,
      162,
      119,
      122
    ]
  },
  "demand_share": {
    "DC1": 0.25,
    "DC2": 0.2,
    "DC3": 0.2,
    "DC4": 0.2,
    "DC5": 0.15
  },
  "costs": {
    "lost_sales": {
      "SKU_Basic": 50.0,
      "SKU_Premium": 160.0,
      "SKU_ShortLife": 40.0
    },
    "inventory": {
      "SKU_Basic": 1.0,
      "SKU_Premium": 1.5,
      "SKU_ShortLife": 1.0
    },
    "waste": {
      "SKU_Basic": 2.0,
      "SKU_Premium": 3.0,
      "SKU_ShortLife": 2.0
    },
    "fixed_order": 0.0,
    "transshipment": 0.5,
    "purchasing": {
      "SKU_Basic": 11.0,
      "SKU_Premium": 16.0,
      "SKU_ShortLife": 15.0
    }
  },
  "constraints": {
    "moq": 0,
    "pack_size": 1,
    "budget_per_period": null,
    "waste_limit_pct": null
  },
  "network": {
    "sub_edges": [
      [
        "SKU_Basic",
        "SKU_Premium"
      ]
    ],
    "trans_edges": []
  }
}
```

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB; import json
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that:
1. Parses the JSON data above (use json.loads on the string)
2. Models and solves the optimization problem
3. Prints status and objective value

Standard seasonal retail scenario.


[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_promo_budget
Scenario ID: retail_f2_promo_budget_v0

[BUSINESS DESCRIPTION]
Business narrative:
In the final periods of the horizon the retailer runs strong promotions on the basic and short-life products, causing their demand to spike. At the same time, an open-to-buy style budget limits how much can be spent in each period on inventory-related activities. The retailer must allocate inventory and spending over time so that promotion-induced demand is supported as well as possible without violating the budget caps.

Structure cues:
- The inventory and substitution structure is the usual single-echelon system with shared storage, production capacity, and a substitution graph, together with lost sales for unmet demand.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- In the last few periods, the JSON scales up demand for the basic and short-life products to represent promotion-induced lifts; these are treated as ordinary exogenous demands with larger magnitudes.
- The JSON also provides a per-period budget for inventory-related spending. For each period, the total purchasing spend (order quantity times purchasing cost) plus any fixed ordering costs if enabled must not exceed this budget.
- Inventory remains non-negative, and any demand that cannot be satisfied within the budget and capacity limits becomes lost sales.
- Substitution behavior, zero lead times, and generous labor capacity remain active as in other Family 2 archetypes.
- The objective is to minimize total cost (aggregating purchasing, holding, waste, and lost sales) subject to the per-period budget, making the trade-off between promotion support and resource limits explicit.

[DATA SCHEMA]
{
  "name": str,                          # scenario identifier
  "periods": int,                       # number of time periods
  "products": [str, ...],               # list of product IDs
  "locations": [str, ...],              # list of location IDs

  "shelf_life": {p: int},               # shelf life in periods per product
  "lead_time": {p: int},                # order lead time per product (0 = same-period arrival)

  "demand_curve": {p: [float, ...]},    # demand per product per period (0-indexed list)
  "demand_share": {l: float},           # fraction of total demand at each location

  "production_cap": {p: [float, ...]},  # max production per product per period (0-indexed list)
  "cold_capacity": {l: float},          # storage capacity per location
  "cold_usage": {p: float},             # storage units per unit of product

  "labor_cap": {l: [float, ...]},       # labor hours per location per period (0-indexed list)
  "labor_usage": {p: float},            # labor hours per unit sold
  "return_rate": {p: float},            # fraction of sales returned next period

  "costs": {
    "purchasing": {p: float},           # cost per unit ordered
    "inventory": {p: float},            # holding cost per unit per period
    "waste": {p: float},                # cost per unit expired
    "lost_sales": {p: float},           # penalty per unit of unmet demand
    "fixed_order": float,               # fixed cost per order placed
    "transshipment": float              # cost per unit transshipped
  },

  "constraints": {
    "moq": float,                       # minimum order quantity (0 = no MOQ)
    "pack_size": int,                   # order must be multiple of this (1 = no constraint)
    "budget_per_period": float|null,    # max purchasing cost per period
    "waste_limit_pct": float|null       # max waste as fraction of total demand
  },

  "network": {
    "sub_edges": [[p_from, p_to], ...], # substitution: p_from's demand can be served by p_to
    "trans_edges": [[l_from, l_to], ...]# transshipment: can ship from l_from to l_to
  }
}

[DATA ACCESS]
- The variable `data` is pre-loaded. Do NOT use file I/O.
- Lists are 0-indexed (period t in model uses index [t-1] in data arrays)

CRITICAL - Network edges require tuple conversion for Gurobi:
  sub_edges = [tuple(e) for e in data.get('network', {}).get('sub_edges', [])]
  trans_edges = [tuple(e) for e in data.get('network', {}).get('trans_edges', [])]

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that models and solves this optimization problem.

[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_promo_budget
Scenario ID: retail_f2_promo_budget_v0

[BUSINESS DESCRIPTION]
Business narrative:
In the final periods of the horizon the retailer runs strong promotions on the basic and short-life products, causing their demand to spike. At the same time, an open-to-buy style budget limits how much can be spent in each period on inventory-related activities. The retailer must allocate inventory and spending over time so that promotion-induced demand is supported as well as possible without violating the budget caps.

Structure cues:
- The inventory and substitution structure is the usual single-echelon system with shared storage, production capacity, and a substitution graph, together with lost sales for unmet demand.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- In the last few periods, the JSON scales up demand for the basic and short-life products to represent promotion-induced lifts; these are treated as ordinary exogenous demands with larger magnitudes.
- The JSON also provides a per-period budget for inventory-related spending. For each period, the total purchasing spend (order quantity times purchasing cost) plus any fixed ordering costs if enabled must not exceed this budget.
- Inventory remains non-negative, and any demand that cannot be satisfied within the budget and capacity limits becomes lost sales.
- Substitution behavior, zero lead times, and generous labor capacity remain active as in other Family 2 archetypes.
- The objective is to minimize total cost (aggregating purchasing, holding, waste, and lost sales) subject to the per-period budget, making the trade-off between promotion support and resource limits explicit.

[DATA]
The following JSON contains all instance data. Parse it directly in your code.

```json
{
  "name": "retail_f2_promo_budget_v0",
  "description": "Standard seasonal retail scenario.",
  "periods": 20,
  "products": [
    "SKU_Basic",
    "SKU_Premium",
    "SKU_ShortLife"
  ],
  "locations": [
    "DC1",
    "DC2",
    "DC3",
    "DC4",
    "DC5"
  ],
  "shelf_life": {
    "SKU_Basic": 10,
    "SKU_Premium": 8,
    "SKU_ShortLife": 4
  },
  "lead_time": {
    "SKU_Basic": 0,
    "SKU_Premium": 0,
    "SKU_ShortLife": 0
  },
  "cold_capacity": {
    "DC1": 4000,
    "DC2": 3500,
    "DC3": 3000,
    "DC4": 3000,
    "DC5": 2500
  },
  "cold_usage": {
    "SKU_Basic": 1.0,
    "SKU_Premium": 3.0,
    "SKU_ShortLife": 1.2
  },
  "production_cap": {
    "SKU_Basic": [
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800
    ],
    "SKU_Premium": [
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400
    ],
    "SKU_ShortLife": [
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500
    ]
  },
  "labor_cap": {
    "DC1": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC2": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC3": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC4": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC5": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ]
  },
  "labor_usage": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "return_rate": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "demand_curve": {
    "SKU_Basic": [
      303,
      311,
      328,
      365,
      435,
      549,
      711,
      906,
      1100,
      1245,
      1300,
      1245,
      1100,
      906,
      711,
      549,
      870,
      730,
      656,
      622
    ],
    "SKU_Premium": [
      151,
      155,
      164,
      182,
      217,
      274,
      355,
      453,
      550,
      622,
      650,
      622,
      550,
      453,
      355,
      274,
      217,
      182,
      164,
      155
    ],
    "SKU_ShortLife": [
      121,
      124,
      131,
      146,
      174,
      219,
      284,
      362,
      440,
      498,
      520,
      498,
      440,
      362,
      284,
      219,
      348,
      292,
      262,
      248
    ]
  },
  "demand_share": {
    "DC1": 0.25,
    "DC2": 0.2,
    "DC3": 0.2,
    "DC4": 0.2,
    "DC5": 0.15
  },
  "costs": {
    "lost_sales": {
      "SKU_Basic": 50.0,
      "SKU_Premium": 80.0,
      "SKU_ShortLife": 40.0
    },
    "inventory": {
      "SKU_Basic": 1.0,
      "SKU_Premium": 1.5,
      "SKU_ShortLife": 1.0
    },
    "waste": {
      "SKU_Basic": 2.0,
      "SKU_Premium": 3.0,
      "SKU_ShortLife": 2.0
    },
    "fixed_order": 0.0,
    "transshipment": 0.5,
    "purchasing": {
      "SKU_Basic": 10.0,
      "SKU_Premium": 20.0,
      "SKU_ShortLife": 15.0
    }
  },
  "constraints": {
    "moq": 0,
    "pack_size": 1,
    "budget_per_period": 15000.0,
    "waste_limit_pct": null
  },
  "network": {
    "sub_edges": [
      [
        "SKU_Basic",
        "SKU_Premium"
      ]
    ],
    "trans_edges": []
  }
}
```

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB; import json
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that:
1. Parses the JSON data above (use json.loads on the string)
2. Models and solves the optimization problem
3. Prints status and objective value

Standard seasonal retail scenario.


[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_promo_budget
Scenario ID: retail_f2_promo_budget_v1

[BUSINESS DESCRIPTION]
Business narrative:
In the final periods of the horizon the retailer runs strong promotions on the basic and short-life products, causing their demand to spike. At the same time, an open-to-buy style budget limits how much can be spent in each period on inventory-related activities. The retailer must allocate inventory and spending over time so that promotion-induced demand is supported as well as possible without violating the budget caps.

Structure cues:
- The inventory and substitution structure is the usual single-echelon system with shared storage, production capacity, and a substitution graph, together with lost sales for unmet demand.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- In the last few periods, the JSON scales up demand for the basic and short-life products to represent promotion-induced lifts; these are treated as ordinary exogenous demands with larger magnitudes.
- The JSON also provides a per-period budget for inventory-related spending. For each period, the total purchasing spend (order quantity times purchasing cost) plus any fixed ordering costs if enabled must not exceed this budget.
- Inventory remains non-negative, and any demand that cannot be satisfied within the budget and capacity limits becomes lost sales.
- Substitution behavior, zero lead times, and generous labor capacity remain active as in other Family 2 archetypes.
- The objective is to minimize total cost (aggregating purchasing, holding, waste, and lost sales) subject to the per-period budget, making the trade-off between promotion support and resource limits explicit.

[DATA SCHEMA]
{
  "name": str,                          # scenario identifier
  "periods": int,                       # number of time periods
  "products": [str, ...],               # list of product IDs
  "locations": [str, ...],              # list of location IDs

  "shelf_life": {p: int},               # shelf life in periods per product
  "lead_time": {p: int},                # order lead time per product (0 = same-period arrival)

  "demand_curve": {p: [float, ...]},    # demand per product per period (0-indexed list)
  "demand_share": {l: float},           # fraction of total demand at each location

  "production_cap": {p: [float, ...]},  # max production per product per period (0-indexed list)
  "cold_capacity": {l: float},          # storage capacity per location
  "cold_usage": {p: float},             # storage units per unit of product

  "labor_cap": {l: [float, ...]},       # labor hours per location per period (0-indexed list)
  "labor_usage": {p: float},            # labor hours per unit sold
  "return_rate": {p: float},            # fraction of sales returned next period

  "costs": {
    "purchasing": {p: float},           # cost per unit ordered
    "inventory": {p: float},            # holding cost per unit per period
    "waste": {p: float},                # cost per unit expired
    "lost_sales": {p: float},           # penalty per unit of unmet demand
    "fixed_order": float,               # fixed cost per order placed
    "transshipment": float              # cost per unit transshipped
  },

  "constraints": {
    "moq": float,                       # minimum order quantity (0 = no MOQ)
    "pack_size": int,                   # order must be multiple of this (1 = no constraint)
    "budget_per_period": float|null,    # max purchasing cost per period
    "waste_limit_pct": float|null       # max waste as fraction of total demand
  },

  "network": {
    "sub_edges": [[p_from, p_to], ...], # substitution: p_from's demand can be served by p_to
    "trans_edges": [[l_from, l_to], ...]# transshipment: can ship from l_from to l_to
  }
}

[DATA ACCESS]
- The variable `data` is pre-loaded. Do NOT use file I/O.
- Lists are 0-indexed (period t in model uses index [t-1] in data arrays)

CRITICAL - Network edges require tuple conversion for Gurobi:
  sub_edges = [tuple(e) for e in data.get('network', {}).get('sub_edges', [])]
  trans_edges = [tuple(e) for e in data.get('network', {}).get('trans_edges', [])]

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that models and solves this optimization problem.

[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_promo_budget
Scenario ID: retail_f2_promo_budget_v1

[BUSINESS DESCRIPTION]
Business narrative:
In the final periods of the horizon the retailer runs strong promotions on the basic and short-life products, causing their demand to spike. At the same time, an open-to-buy style budget limits how much can be spent in each period on inventory-related activities. The retailer must allocate inventory and spending over time so that promotion-induced demand is supported as well as possible without violating the budget caps.

Structure cues:
- The inventory and substitution structure is the usual single-echelon system with shared storage, production capacity, and a substitution graph, together with lost sales for unmet demand.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- In the last few periods, the JSON scales up demand for the basic and short-life products to represent promotion-induced lifts; these are treated as ordinary exogenous demands with larger magnitudes.
- The JSON also provides a per-period budget for inventory-related spending. For each period, the total purchasing spend (order quantity times purchasing cost) plus any fixed ordering costs if enabled must not exceed this budget.
- Inventory remains non-negative, and any demand that cannot be satisfied within the budget and capacity limits becomes lost sales.
- Substitution behavior, zero lead times, and generous labor capacity remain active as in other Family 2 archetypes.
- The objective is to minimize total cost (aggregating purchasing, holding, waste, and lost sales) subject to the per-period budget, making the trade-off between promotion support and resource limits explicit.

[DATA]
The following JSON contains all instance data. Parse it directly in your code.

```json
{
  "name": "retail_f2_promo_budget_v1",
  "description": "Standard seasonal retail scenario.",
  "periods": 20,
  "products": [
    "SKU_Basic",
    "SKU_Premium",
    "SKU_ShortLife"
  ],
  "locations": [
    "DC1",
    "DC2",
    "DC3",
    "DC4",
    "DC5"
  ],
  "shelf_life": {
    "SKU_Basic": 10,
    "SKU_Premium": 8,
    "SKU_ShortLife": 4
  },
  "lead_time": {
    "SKU_Basic": 0,
    "SKU_Premium": 0,
    "SKU_ShortLife": 0
  },
  "cold_capacity": {
    "DC1": 3927.796111686252,
    "DC2": 3664.3480700069595,
    "DC3": 3239.291492965392,
    "DC4": 2648.7276547218867,
    "DC5": 2359.990813239097
  },
  "cold_usage": {
    "SKU_Basic": 1.0,
    "SKU_Premium": 3.0,
    "SKU_ShortLife": 1.2
  },
  "production_cap": {
    "SKU_Basic": [
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800
    ],
    "SKU_Premium": [
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400
    ],
    "SKU_ShortLife": [
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500
    ]
  },
  "labor_cap": {
    "DC1": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC2": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC3": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC4": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC5": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ]
  },
  "labor_usage": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "return_rate": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "demand_curve": {
    "SKU_Basic": [
      341,
      318,
      355,
      415,
      497,
      597,
      775,
      919,
      1074,
      1120,
      1295,
      1237,
      1122,
      796,
      657,
      544,
      811,
      822,
      584,
      534
    ],
    "SKU_Premium": [
      155,
      163,
      151,
      205,
      211,
      261,
      340,
      397,
      515,
      681,
      670,
      589,
      531,
      401,
      377,
      256,
      208,
      174,
      143,
      157
    ],
    "SKU_ShortLife": [
      107,
      115,
      127,
      167,
      168,
      245,
      249,
      392,
      380,
      559,
      481,
      493,
      455,
      348,
      277,
      245,
      314,
      250,
      230,
      239
    ]
  },
  "demand_share": {
    "DC1": 0.25,
    "DC2": 0.2,
    "DC3": 0.2,
    "DC4": 0.2,
    "DC5": 0.15
  },
  "costs": {
    "lost_sales": {
      "SKU_Basic": 50.0,
      "SKU_Premium": 80.0,
      "SKU_ShortLife": 40.0
    },
    "inventory": {
      "SKU_Basic": 1.0,
      "SKU_Premium": 1.5,
      "SKU_ShortLife": 1.0
    },
    "waste": {
      "SKU_Basic": 2.0,
      "SKU_Premium": 3.0,
      "SKU_ShortLife": 2.0
    },
    "fixed_order": 0.0,
    "transshipment": 0.5,
    "purchasing": {
      "SKU_Basic": 10.0,
      "SKU_Premium": 20.0,
      "SKU_ShortLife": 15.0
    }
  },
  "constraints": {
    "moq": 0,
    "pack_size": 1,
    "budget_per_period": 15000.0,
    "waste_limit_pct": null
  },
  "network": {
    "sub_edges": [
      [
        "SKU_Basic",
        "SKU_Premium"
      ]
    ],
    "trans_edges": []
  }
}
```

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB; import json
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that:
1. Parses the JSON data above (use json.loads on the string)
2. Models and solves the optimization problem
3. Prints status and objective value

Standard seasonal retail scenario.


[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_promo_budget
Scenario ID: retail_f2_promo_budget_v2

[BUSINESS DESCRIPTION]
Business narrative:
In the final periods of the horizon the retailer runs strong promotions on the basic and short-life products, causing their demand to spike. At the same time, an open-to-buy style budget limits how much can be spent in each period on inventory-related activities. The retailer must allocate inventory and spending over time so that promotion-induced demand is supported as well as possible without violating the budget caps.

Structure cues:
- The inventory and substitution structure is the usual single-echelon system with shared storage, production capacity, and a substitution graph, together with lost sales for unmet demand.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- In the last few periods, the JSON scales up demand for the basic and short-life products to represent promotion-induced lifts; these are treated as ordinary exogenous demands with larger magnitudes.
- The JSON also provides a per-period budget for inventory-related spending. For each period, the total purchasing spend (order quantity times purchasing cost) plus any fixed ordering costs if enabled must not exceed this budget.
- Inventory remains non-negative, and any demand that cannot be satisfied within the budget and capacity limits becomes lost sales.
- Substitution behavior, zero lead times, and generous labor capacity remain active as in other Family 2 archetypes.
- The objective is to minimize total cost (aggregating purchasing, holding, waste, and lost sales) subject to the per-period budget, making the trade-off between promotion support and resource limits explicit.

[DATA SCHEMA]
{
  "name": str,                          # scenario identifier
  "periods": int,                       # number of time periods
  "products": [str, ...],               # list of product IDs
  "locations": [str, ...],              # list of location IDs

  "shelf_life": {p: int},               # shelf life in periods per product
  "lead_time": {p: int},                # order lead time per product (0 = same-period arrival)

  "demand_curve": {p: [float, ...]},    # demand per product per period (0-indexed list)
  "demand_share": {l: float},           # fraction of total demand at each location

  "production_cap": {p: [float, ...]},  # max production per product per period (0-indexed list)
  "cold_capacity": {l: float},          # storage capacity per location
  "cold_usage": {p: float},             # storage units per unit of product

  "labor_cap": {l: [float, ...]},       # labor hours per location per period (0-indexed list)
  "labor_usage": {p: float},            # labor hours per unit sold
  "return_rate": {p: float},            # fraction of sales returned next period

  "costs": {
    "purchasing": {p: float},           # cost per unit ordered
    "inventory": {p: float},            # holding cost per unit per period
    "waste": {p: float},                # cost per unit expired
    "lost_sales": {p: float},           # penalty per unit of unmet demand
    "fixed_order": float,               # fixed cost per order placed
    "transshipment": float              # cost per unit transshipped
  },

  "constraints": {
    "moq": float,                       # minimum order quantity (0 = no MOQ)
    "pack_size": int,                   # order must be multiple of this (1 = no constraint)
    "budget_per_period": float|null,    # max purchasing cost per period
    "waste_limit_pct": float|null       # max waste as fraction of total demand
  },

  "network": {
    "sub_edges": [[p_from, p_to], ...], # substitution: p_from's demand can be served by p_to
    "trans_edges": [[l_from, l_to], ...]# transshipment: can ship from l_from to l_to
  }
}

[DATA ACCESS]
- The variable `data` is pre-loaded. Do NOT use file I/O.
- Lists are 0-indexed (period t in model uses index [t-1] in data arrays)

CRITICAL - Network edges require tuple conversion for Gurobi:
  sub_edges = [tuple(e) for e in data.get('network', {}).get('sub_edges', [])]
  trans_edges = [tuple(e) for e in data.get('network', {}).get('trans_edges', [])]

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that models and solves this optimization problem.

[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_promo_budget
Scenario ID: retail_f2_promo_budget_v2

[BUSINESS DESCRIPTION]
Business narrative:
In the final periods of the horizon the retailer runs strong promotions on the basic and short-life products, causing their demand to spike. At the same time, an open-to-buy style budget limits how much can be spent in each period on inventory-related activities. The retailer must allocate inventory and spending over time so that promotion-induced demand is supported as well as possible without violating the budget caps.

Structure cues:
- The inventory and substitution structure is the usual single-echelon system with shared storage, production capacity, and a substitution graph, together with lost sales for unmet demand.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- In the last few periods, the JSON scales up demand for the basic and short-life products to represent promotion-induced lifts; these are treated as ordinary exogenous demands with larger magnitudes.
- The JSON also provides a per-period budget for inventory-related spending. For each period, the total purchasing spend (order quantity times purchasing cost) plus any fixed ordering costs if enabled must not exceed this budget.
- Inventory remains non-negative, and any demand that cannot be satisfied within the budget and capacity limits becomes lost sales.
- Substitution behavior, zero lead times, and generous labor capacity remain active as in other Family 2 archetypes.
- The objective is to minimize total cost (aggregating purchasing, holding, waste, and lost sales) subject to the per-period budget, making the trade-off between promotion support and resource limits explicit.

[DATA]
The following JSON contains all instance data. Parse it directly in your code.

```json
{
  "name": "retail_f2_promo_budget_v2",
  "description": "Standard seasonal retail scenario.",
  "periods": 20,
  "products": [
    "SKU_Basic",
    "SKU_Premium",
    "SKU_ShortLife"
  ],
  "locations": [
    "DC1",
    "DC2",
    "DC3",
    "DC4",
    "DC5"
  ],
  "shelf_life": {
    "SKU_Basic": 10,
    "SKU_Premium": 8,
    "SKU_ShortLife": 4
  },
  "lead_time": {
    "SKU_Basic": 0,
    "SKU_Premium": 0,
    "SKU_ShortLife": 0
  },
  "cold_capacity": {
    "DC1": 3514.0049602177187,
    "DC2": 2978.815548884458,
    "DC3": 3270.184260527497,
    "DC4": 3141.730615435521,
    "DC5": 2678.8726331690727
  },
  "cold_usage": {
    "SKU_Basic": 1.0,
    "SKU_Premium": 3.0,
    "SKU_ShortLife": 1.2
  },
  "production_cap": {
    "SKU_Basic": [
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800
    ],
    "SKU_Premium": [
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400
    ],
    "SKU_ShortLife": [
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500
    ]
  },
  "labor_cap": {
    "DC1": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC2": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC3": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC4": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC5": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ]
  },
  "labor_usage": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "return_rate": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "demand_curve": {
    "SKU_Basic": [
      267,
      279,
      356,
      417,
      462,
      581,
      661,
      939,
      1182,
      1233,
      1321,
      1426,
      1082,
      929,
      749,
      543,
      956,
      812,
      721,
      692
    ],
    "SKU_Premium": [
      163,
      158,
      167,
      160,
      197,
      245,
      310,
      520,
      518,
      616,
      583,
      548,
      477,
      510,
      351,
      235,
      192,
      207,
      154,
      177
    ],
    "SKU_ShortLife": [
      137,
      134,
      139,
      131,
      181,
      234,
      306,
      339,
      489,
      471,
      597,
      533,
      403,
      407,
      248,
      203,
      370,
      269,
      288,
      268
    ]
  },
  "demand_share": {
    "DC1": 0.25,
    "DC2": 0.2,
    "DC3": 0.2,
    "DC4": 0.2,
    "DC5": 0.15
  },
  "costs": {
    "lost_sales": {
      "SKU_Basic": 50.0,
      "SKU_Premium": 80.0,
      "SKU_ShortLife": 40.0
    },
    "inventory": {
      "SKU_Basic": 1.0,
      "SKU_Premium": 1.5,
      "SKU_ShortLife": 1.0
    },
    "waste": {
      "SKU_Basic": 2.0,
      "SKU_Premium": 3.0,
      "SKU_ShortLife": 2.0
    },
    "fixed_order": 0.0,
    "transshipment": 0.5,
    "purchasing": {
      "SKU_Basic": 10.0,
      "SKU_Premium": 20.0,
      "SKU_ShortLife": 15.0
    }
  },
  "constraints": {
    "moq": 0,
    "pack_size": 1,
    "budget_per_period": 15000.0,
    "waste_limit_pct": null
  },
  "network": {
    "sub_edges": [
      [
        "SKU_Basic",
        "SKU_Premium"
      ]
    ],
    "trans_edges": []
  }
}
```

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB; import json
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that:
1. Parses the JSON data above (use json.loads on the string)
2. Models and solves the optimization problem
3. Prints status and objective value

Standard seasonal retail scenario.


[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_promo_budget
Scenario ID: retail_f2_promo_budget_v3

[BUSINESS DESCRIPTION]
Business narrative:
In the final periods of the horizon the retailer runs strong promotions on the basic and short-life products, causing their demand to spike. At the same time, an open-to-buy style budget limits how much can be spent in each period on inventory-related activities. The retailer must allocate inventory and spending over time so that promotion-induced demand is supported as well as possible without violating the budget caps.

Structure cues:
- The inventory and substitution structure is the usual single-echelon system with shared storage, production capacity, and a substitution graph, together with lost sales for unmet demand.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- In the last few periods, the JSON scales up demand for the basic and short-life products to represent promotion-induced lifts; these are treated as ordinary exogenous demands with larger magnitudes.
- The JSON also provides a per-period budget for inventory-related spending. For each period, the total purchasing spend (order quantity times purchasing cost) plus any fixed ordering costs if enabled must not exceed this budget.
- Inventory remains non-negative, and any demand that cannot be satisfied within the budget and capacity limits becomes lost sales.
- Substitution behavior, zero lead times, and generous labor capacity remain active as in other Family 2 archetypes.
- The objective is to minimize total cost (aggregating purchasing, holding, waste, and lost sales) subject to the per-period budget, making the trade-off between promotion support and resource limits explicit.

[DATA SCHEMA]
{
  "name": str,                          # scenario identifier
  "periods": int,                       # number of time periods
  "products": [str, ...],               # list of product IDs
  "locations": [str, ...],              # list of location IDs

  "shelf_life": {p: int},               # shelf life in periods per product
  "lead_time": {p: int},                # order lead time per product (0 = same-period arrival)

  "demand_curve": {p: [float, ...]},    # demand per product per period (0-indexed list)
  "demand_share": {l: float},           # fraction of total demand at each location

  "production_cap": {p: [float, ...]},  # max production per product per period (0-indexed list)
  "cold_capacity": {l: float},          # storage capacity per location
  "cold_usage": {p: float},             # storage units per unit of product

  "labor_cap": {l: [float, ...]},       # labor hours per location per period (0-indexed list)
  "labor_usage": {p: float},            # labor hours per unit sold
  "return_rate": {p: float},            # fraction of sales returned next period

  "costs": {
    "purchasing": {p: float},           # cost per unit ordered
    "inventory": {p: float},            # holding cost per unit per period
    "waste": {p: float},                # cost per unit expired
    "lost_sales": {p: float},           # penalty per unit of unmet demand
    "fixed_order": float,               # fixed cost per order placed
    "transshipment": float              # cost per unit transshipped
  },

  "constraints": {
    "moq": float,                       # minimum order quantity (0 = no MOQ)
    "pack_size": int,                   # order must be multiple of this (1 = no constraint)
    "budget_per_period": float|null,    # max purchasing cost per period
    "waste_limit_pct": float|null       # max waste as fraction of total demand
  },

  "network": {
    "sub_edges": [[p_from, p_to], ...], # substitution: p_from's demand can be served by p_to
    "trans_edges": [[l_from, l_to], ...]# transshipment: can ship from l_from to l_to
  }
}

[DATA ACCESS]
- The variable `data` is pre-loaded. Do NOT use file I/O.
- Lists are 0-indexed (period t in model uses index [t-1] in data arrays)

CRITICAL - Network edges require tuple conversion for Gurobi:
  sub_edges = [tuple(e) for e in data.get('network', {}).get('sub_edges', [])]
  trans_edges = [tuple(e) for e in data.get('network', {}).get('trans_edges', [])]

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that models and solves this optimization problem.

[SCENARIO]
Family: F2 (Assortment and Substitution)
Archetype: retail_f2_promo_budget
Scenario ID: retail_f2_promo_budget_v3

[BUSINESS DESCRIPTION]
Business narrative:
In the final periods of the horizon the retailer runs strong promotions on the basic and short-life products, causing their demand to spike. At the same time, an open-to-buy style budget limits how much can be spent in each period on inventory-related activities. The retailer must allocate inventory and spending over time so that promotion-induced demand is supported as well as possible without violating the budget caps.

Structure cues:
- The inventory and substitution structure is the usual single-echelon system with shared storage, production capacity, and a substitution graph, together with lost sales for unmet demand.
- Shelf life: Each product has a shelf life in periods. Inventory must be tracked by REMAINING LIFE.

  VARIABLE DEFINITION: I[p,l,t,r] = inventory at START of period t with r periods remaining.
  Convention: r=1 is OLDEST (sell first FIFO), r=shelf_life[p] is FRESHEST.

  KEY EQUATIONS - implement EXACTLY as written, do NOT add or remove terms:

  (1) Fresh inflow: I[p,l,t,SL] = Q[p,t] * demand_share[l]
      - This is ONLY the inflow from production. Do NOT subtract sales here!
      - Q is total production, demand_share distributes it to locations.

  (2) Aging: I[p,l,t+1,r] = I[p,l,t,r+1] - sales[p,l,t,r+1] for r=1..SL-1
      - Inventory tomorrow with r remaining = inventory today with r+1 remaining - sales

  (3) Waste: W[p,l,t] = I[p,l,t,1] - sales[p,l,t,1]
      - Items with r=1 that aren't sold become waste

  (4) Sales availability: sales[p,l,t,r] <= I[p,l,t,r]
      - Can only sell what you have in inventory

  (5) Inventory holding cost: charged on (I[p,l,t,r] - sales[p,l,t,r]) for r >= 2
      - Charge on END-of-period inventory (after sales), exclude expiring items (r=1)
- Storage capacity: sum over products of (cold_usage[p] * total_inventory[p,l,t]) <= cold_capacity[l]. These limits must be respected.
- In the last few periods, the JSON scales up demand for the basic and short-life products to represent promotion-induced lifts; these are treated as ordinary exogenous demands with larger magnitudes.
- The JSON also provides a per-period budget for inventory-related spending. For each period, the total purchasing spend (order quantity times purchasing cost) plus any fixed ordering costs if enabled must not exceed this budget.
- Inventory remains non-negative, and any demand that cannot be satisfied within the budget and capacity limits becomes lost sales.
- Substitution behavior, zero lead times, and generous labor capacity remain active as in other Family 2 archetypes.
- The objective is to minimize total cost (aggregating purchasing, holding, waste, and lost sales) subject to the per-period budget, making the trade-off between promotion support and resource limits explicit.

[DATA]
The following JSON contains all instance data. Parse it directly in your code.

```json
{
  "name": "retail_f2_promo_budget_v3",
  "description": "Standard seasonal retail scenario.",
  "periods": 20,
  "products": [
    "SKU_Basic",
    "SKU_Premium",
    "SKU_ShortLife"
  ],
  "locations": [
    "DC1",
    "DC2",
    "DC3",
    "DC4",
    "DC5"
  ],
  "shelf_life": {
    "SKU_Basic": 10,
    "SKU_Premium": 8,
    "SKU_ShortLife": 4
  },
  "lead_time": {
    "SKU_Basic": 0,
    "SKU_Premium": 0,
    "SKU_ShortLife": 0
  },
  "cold_capacity": {
    "DC1": 4067.504721874054,
    "DC2": 3364.679049680995,
    "DC3": 3226.0592560031964,
    "DC4": 2560.436070515946,
    "DC5": 2343.357719960569
  },
  "cold_usage": {
    "SKU_Basic": 1.0,
    "SKU_Premium": 3.0,
    "SKU_ShortLife": 1.2
  },
  "production_cap": {
    "SKU_Basic": [
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800,
      800
    ],
    "SKU_Premium": [
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400,
      400
    ],
    "SKU_ShortLife": [
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500,
      500
    ]
  },
  "labor_cap": {
    "DC1": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC2": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC3": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC4": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ],
    "DC5": [
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0,
      99999.0
    ]
  },
  "labor_usage": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "return_rate": {
    "SKU_Basic": 0.0,
    "SKU_Premium": 0.0,
    "SKU_ShortLife": 0.0
  },
  "demand_curve": {
    "SKU_Basic": [
      298,
      310,
      290,
      355,
      424,
      582,
      772,
      947,
      1126,
      1354,
      1227,
      1331,
      1080,
      1007,
      816,
      621,
      747,
      705,
      592,
      571
    ],
    "SKU_Premium": [
      148,
      147,
      164,
      200,
      201,
      291,
      353,
      404,
      545,
      547,
      610,
      690,
      573,
      460,
      337,
      241,
      239,
      172,
      158,
      160
    ],
    "SKU_ShortLife": [
      135,
      141,
      134,
      140,
      164,
      207,
      286,
      344,
      440,
      457,
      460,
      535,
      497,
      355,
      251,
      225,
      304,
      259,
      293,
      260
    ]
  },
  "demand_share": {
    "DC1": 0.25,
    "DC2": 0.2,
    "DC3": 0.2,
    "DC4": 0.2,
    "DC5": 0.15
  },
  "costs": {
    "lost_sales": {
      "SKU_Basic": 50.0,
      "SKU_Premium": 80.0,
      "SKU_ShortLife": 40.0
    },
    "inventory": {
      "SKU_Basic": 1.0,
      "SKU_Premium": 1.5,
      "SKU_ShortLife": 1.0
    },
    "waste": {
      "SKU_Basic": 2.0,
      "SKU_Premium": 3.0,
      "SKU_ShortLife": 2.0
    },
    "fixed_order": 0.0,
    "transshipment": 0.5,
    "purchasing": {
      "SKU_Basic": 10.0,
      "SKU_Premium": 20.0,
      "SKU_ShortLife": 15.0
    }
  },
  "constraints": {
    "moq": 0,
    "pack_size": 1,
    "budget_per_period": 15000.0,
    "waste_limit_pct": null
  },
  "network": {
    "sub_edges": [
      [
        "SKU_Basic",
        "SKU_Premium"
      ]
    ],
    "trans_edges": []
  }
}
```

[OUTPUT FORMAT]
- Import: import gurobipy as gp; from gurobipy import GRB; import json
- Set Gurobi params: m.Params.OutputFlag = 0; m.Params.Threads = 1; m.Params.Seed = 0
- Print at end:
  print(f"status: {{m.Status}}")
  if m.Status == 2:
      print(f"objective: {{m.ObjVal}}")
- Output ONLY executable Python code. No markdown, no explanations.

[TASK]
Write a GurobiPy script that:
1. Parses the JSON data above (use json.loads on the string)
2. Models and solves the optimization problem
3. Prints status and objective value

Standard seasonal retail scenario.