Skip to content

Commit e898eab

Browse files
committed
Improve example fpr callback to notebook
1 parent 7d42744 commit e898eab

1 file changed

Lines changed: 62 additions & 22 deletions

File tree

docs/notebooks/04-operational-constraints.ipynb

Lines changed: 62 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -422,9 +422,9 @@
422422
"\n",
423423
"Sometimes you need constraints beyond what's built into the components. The `before_solve` callback lets you add custom constraints directly to the optimization model.\n",
424424
"\n",
425-
"### Example: Limit Main Boiler Startups\n",
425+
"### Example: Ramp Rate Limits\n",
426426
"\n",
427-
"Let's limit the main boiler to a maximum of 3 startups:"
427+
"Large boilers can't change output instantly—thermal stress limits how fast they can ramp up or down. Let's add a constraint limiting the main boiler to ±50 kW change per timestep:"
428428
]
429429
},
430430
{
@@ -434,19 +434,24 @@
434434
"metadata": {},
435435
"outputs": [],
436436
"source": [
437-
"# Build a fresh system with status tracking on both boilers\n",
438-
"fs_custom = flow_system.copy()\n",
439-
"fs_custom.reset()\n",
437+
"fs_ramp = flow_system.copy()\n",
440438
"\n",
441439
"\n",
442-
"def limit_main_boiler_startups(fs):\n",
443-
" \"\"\"Custom constraint: Main boiler can start at most 3 times.\"\"\"\n",
440+
"def add_ramp_rate_limit(fs, max_ramp: float = 10):\n",
441+
" \"\"\"Limit ramp rate when boiler stays on. Uses Big-M to allow on/off jumps.\"\"\"\n",
444442
" model = fs.model\n",
445-
" main_startups = model.variables['MainBoiler|startup']\n",
446-
" model.add_constraints(main_startups.sum() <= 3, name='max_main_startups')\n",
443+
" flow = model.variables['MainBoiler(Steam)|flow_rate']\n",
444+
" status = model.variables['MainBoiler|status']\n",
447445
"\n",
446+
" ramp = flow - flow.shift(time=1)\n",
447+
" both_on = status + status.shift(time=1) # =2 when both on, <2 otherwise\n",
448448
"\n",
449-
"fs_custom.optimize(fx.solvers.HighsSolver(mip_gap=0.01), before_solve=limit_main_boiler_startups);"
449+
" big_m = 500 # Big-M (larger than max flow)\n",
450+
" model.add_constraints(ramp <= max_ramp + big_m * (2 - both_on), name='ramp_up')\n",
451+
" model.add_constraints(ramp >= -max_ramp - big_m * (2 - both_on), name='ramp_down')\n",
452+
"\n",
453+
"\n",
454+
"fs_ramp.optimize(fx.solvers.HighsSolver(mip_gap=0.01), before_solve=add_ramp_rate_limit);"
450455
]
451456
},
452457
{
@@ -456,16 +461,14 @@
456461
"metadata": {},
457462
"outputs": [],
458463
"source": [
459-
"# Verify: Main boiler startups are limited\n",
460-
"main_startups = int(fs_custom.solution['MainBoiler|startup'].sum().item())\n",
461-
"print(f'MainBoiler startups: {main_startups} (limit: 3)')\n",
462-
"\n",
463-
"# Compare with unconstrained (which had more startups)\n",
464-
"print(f'Original system startups: {total_startups}')\n",
465-
"\n",
466-
"# Show startup events\n",
467-
"startup_ds = xr.Dataset({'MainBoiler startup': fs_custom.solution['MainBoiler|startup']})\n",
468-
"startup_ds.plotly.line(x='time', title='Main Boiler Startup Events', height=250)"
464+
"# Compare: with vs without ramp rate limits\n",
465+
"comparison_ds = xr.Dataset(\n",
466+
" {\n",
467+
" 'Without ramp limit': flow_system.solution['MainBoiler(Steam)|flow_rate'],\n",
468+
" 'With ramp limit (±10 kW)': fs_ramp.solution['MainBoiler(Steam)|flow_rate'],\n",
469+
" }\n",
470+
")\n",
471+
"comparison_ds.plotly.line(x='time', title='Main Boiler Output: Effect of Ramp Rate Limits', height=350)"
469472
]
470473
},
471474
{
@@ -484,16 +487,53 @@
484487
"id": "32",
485488
"metadata": {},
486489
"outputs": [],
490+
"source": [
491+
"# Calculate actual ramp rates (change between timesteps)\n",
492+
"flow_original = flow_system.solution['MainBoiler(Steam)|flow_rate']\n",
493+
"flow_ramp = fs_ramp.solution['MainBoiler(Steam)|flow_rate']\n",
494+
"\n",
495+
"ramp_original = flow_original.diff('time')\n",
496+
"ramp_limited = flow_ramp.diff('time')\n",
497+
"\n",
498+
"print(f'Without ramp limit: max ramp = {abs(ramp_original).max().item():.1f} kW/step')\n",
499+
"print(f'With ramp limit: max ramp = {abs(ramp_limited).max().item():.1f} kW/step (limit: 50 kW)')\n",
500+
"\n",
501+
"# Show the ramp rates over time\n",
502+
"ramp_ds = xr.Dataset(\n",
503+
" {\n",
504+
" 'Original ramp rate': ramp_original,\n",
505+
" 'Limited ramp rate': ramp_limited,\n",
506+
" }\n",
507+
")\n",
508+
"ramp_ds.plotly.line(x='time', title='Ramp Rates (kW change per timestep)', height=300)"
509+
]
510+
},
511+
{
512+
"cell_type": "markdown",
513+
"id": "33",
514+
"metadata": {},
515+
"source": [
516+
"### Finding Available Variables\n",
517+
"\n",
518+
"To discover what variables you can use in custom constraints, inspect the model after building:"
519+
]
520+
},
521+
{
522+
"cell_type": "code",
523+
"execution_count": null,
524+
"id": "34",
525+
"metadata": {},
526+
"outputs": [],
487527
"source": [
488528
"# List all variables in the model\n",
489529
"print('Available variables:')\n",
490-
"for name in fs_custom.model.variables:\n",
530+
"for name in fs_ramp.model.variables:\n",
491531
" print(f' {name}')"
492532
]
493533
},
494534
{
495535
"cell_type": "markdown",
496-
"id": "33",
536+
"id": "35",
497537
"metadata": {},
498538
"source": [
499539
"## Key Concepts\n",

0 commit comments

Comments
 (0)