Hands-On Mathematical Optimization with Python Companion Notebooks

repository·main·Indexed 42 days ago

https://github.com/mobook/mo-book

Companion Jupyter notebooks for the book 'Hands-On Mathematical Optimization with Python' (Cambridge University Press, 2025). This resource provides practical examples for mathematical optimization using Pyomo and the HiGHS solver, covering ConcreteModel construction, decision variables, objectives, constraints, and advanced techniques like matrix-form optimization and minimax linearization.

Tokens
26.3K
Snippets
70
Records
84
Agent score
54%

What's inside Hands-On Mathematical Optimization with Python

  1. Access the companion Jupyter notebooks for Hands-On Mathematical Optimization with Python

    main

    This repository contains the source Jupyter notebooks associated with the book Hands-On Mathematical Optimization with Python (Cambridge University Press, 2025). You can view the rendered collection of notebooks online at the official project website.

    http://mobook.github.io/MO-book/
  2. Model multilayer insulation as a Mixed Integer Quadratic Constraint Optimization (MIQCO)

    main

    To optimize the design of multiple insulation layers, you can formulate a Mixed Integer Quadratic Constraint Optimization (MIQCO) problem. This approach handles the trade-off between annual energy costs and capital installation costs.

    Mathematical Formulation

    • Objective: Minimize $C = \alpha U + \beta \sum_{n=1}^N (a_n y_n + b_n x_n)$
    • Variables:
      • $x_n$: Continuous variable for thickness of layer $n$.
      • $y_n$: Binary variable indicating if layer $n$ is included.
      • $U$: Overall heat transfer coefficient (epigraph variable).
      • $R$: Total thermal resistance.
    • Constraints:
      • $R = R_0 + \sum_{n=1}^N \frac{x_n}{k_n}$
      • $x_n \leq T y_n$ (links thickness to binary inclusion)
      • $\sum_{n=1}^N x_n \leq T$ (total thickness limit)
      • $R \cdot U \geq 1$ (nonlinear relationship, modeled as a quadratic constraint or rotated cone)

    Implementation Pattern

    Use pyo.ConcreteModel to define the set of layers, decision variables (including pyo.Binary for $y_n$), and the quadratic constraint m.R * m.U >= 1 for SCIP.

    def insulate(df, alpha, beta, R0, T):
        m = pyo.ConcreteModel()
        m.N = pyo.Set(initialize=df.index)
        a, b, k = df["a"], df["b"], df["k"]
    
        # decision variables and physical bounds
        Rmax = R0 + T / k.min()
        m.R = pyo.Var(bounds=(R0, Rmax))
        m.U = pyo.Var(bounds=(1 / Rmax, 1 / R0))
        m.x = pyo.Var(m.N, bounds=(0, T))
        m.y = pyo.Var(m.N, domain=pyo.Binary)
    
        m.cost = pyo.Objective(
            expr=alpha * m.U + beta * sum(a[n] * m.y[n] + b[n] * m.x[n] for n in m.N)
        )
        m.insulation = pyo.Constraint(expr=m.R == R0 + sum(m.x[n] / k[n] for n in m.N))
        m.thickness = pyo.Constraint(expr=sum(m.x[n] for n in m.N) <= T)
        m.layers = pyo.Constraint(m.N, rule=lambda m, n: m.x[n] <= T * m.y[n])
        m.q = pyo.Constraint(expr=m.R * m.U >= 1)
        return m
  3. Use PyomoMonad to chain optimization workflows

    main

    The PyomoMonad class manages the state of a Pyomo optimization workflow, including the model, any errors (status), and an optional reporting effect. It allows you to chain model construction, transformations, and solving steps while automatically handling exceptions.

    Key Methods

    • .bind(func): Chains a function that accepts and returns a Pyomo model. If a previous step failed, subsequent bind calls are skipped.
    • >>: An overloaded operator that acts as a shorthand for .bind().
    • __call__(): Executes the monad. If an effect (reporting function) was provided during initialization, it runs that effect on the final model. Otherwise, it returns the model. If the workflow failed, it prints the captured exception.

    Workflow Pattern

    1. Model Builder: A function that returns PyomoMonad(model, effect=report_func).
    2. Chaining: Use .bind(transformer(...)) and .bind(solver(...)) to progress the model.
    3. Execution: Call the resulting monad to get the report or the model.
    # Example workflow using the >> operator
    # ode_model_builder returns a PyomoMonad
    # transformer('dae.collocation') and solver('ipopt') are wrapped functions
    
    (soln := ode_model_builder() >> transformer('dae.collocation') >> solver('ipopt'))()
    
    # Accessing results (assuming effect returns values)
    tf, b = soln
  4. How data-driven modeling works in Pyomo

    main

    Data-driven modeling in Pyomo involves decoupling the problem data from the mathematical structure. Instead of hard-coding values, you use:

    1. Python Data Structures: Dictionaries or DataFrames to hold raw data.
    2. Pyomo Sets: To define the indices (e.g., Products, Resources).
    3. Pyomo Parameters: To map the raw data into the model via rules.
    4. Indexed Components: Variables and constraints that iterate over the Sets.

    This approach allows the model to scale automatically when the input data changes (e.g., adding a new product only requires updating the input dictionary, not the model logic).

    # The pattern: Data -> Sets -> Params -> Vars/Constraints
    products = {"U": {"price": 270, "demand": 40}}
    model.PRODUCTS = pyo.Set(initialize=products.keys())
    
    @model.Param(model.PRODUCTS)
    def cp(model, p):
        return products[p]["price"]
  5. Organize model data using Tidy Data principles

    main

    For managing datasets associated with Pyomo models, use the Tidy Data approach. This ensures that data is structured predictably for reading, writing, and visualization.

    Tidy Data Principles:

    • Each column contains all data associated with a single variable.
    • Each row contains all values for a single observation.
    • Each entry is a single value.

    Implementation Patterns:

    • Pandas DataFrames: Highly recommended for reading, writing, and visualizing Tidy Data.
    • Nested Dictionaries: When using Python dictionaries, use the primary key for the unique identifier of the observation and secondary keys for the variables.

    Example of Tidy Nested Dictionaries:

    scenarios = {
      "high": {"demand": 200, "price": 20},
      "medium": {"demand": 100, "price": 18},
      "low": {"demand": 50, "price": 15}
    }

    In this structure, data is accessed via data[observation][variable] (e.g., scenarios["high"]["demand"]).

  6. Use `pyo.ConcreteModel` instead of `pyo.AbstractModel`

    main

    The preferred method for creating Pyomo model instances is using pyo.ConcreteModel. While pyo.AbstractModel allows for symbolic parameters to be defined before data is provided, pyo.ConcreteModel is more idiomatic when working within Python, as it allows you to use Python functions or classes to pass parameter values during model creation.

    # Preferred approach: create a function that returns a ConcreteModel
    def build_model(param_value):
        model = pyo.ConcreteModel()
        model.p = pyo.Param(initialize=param_value)
        return model
  7. Handle multi-dimensional data with Tidy Data

    main

    When dealing with $n$-dimensional data (e.g., distances between multiple warehouses and customers), avoid wide tables where variables are spread across multiple columns. Instead, reorganize the data into a long format where every value is in a single column, indexed by multiple identifier columns.

    Incorrect (Wide Format):

    Customer 1Customer 2
    Warehouse A254173

    Correct (Tidy/Long Format):

    WarehouseCustomerDistance
    Warehouse ACustomer 1254
    Warehouse ACustomer 2173

    This structure is easier to manipulate with tools like Pandas and prevents complex 'data wrangling' logic from being mixed into your Pyomo model building code.

  8. Subclassing ConcreteModel for specialized applications

    main

    For complex or repetitive modeling tasks, you can subclass pyo.ConcreteModel. This allows you to encapsulate the model building, solving, and reporting logic into a single reusable object.

    When subclassing:

    • Call super().__init__() in __init__.
    • Implement a build_model() method to define the Pyomo components (Sets, Params, Vars, etc.).
    • Use the class to manage the lifecycle of the optimization task.
    class ProductionModel(pyo.ConcreteModel):
        def __init__(self, products, resources, processes):
            super().__init__("Production Model")
            self.products = products
            # ... store other data
    
        def build_model(self):
            # Define Pyomo components here using self.products, etc.
            self.PRODUCTS = pyo.Set(initialize=self.products.keys())
            # ...
    
        def solve(self, solver):
            self.build_model()
            solver.solve(self)
  9. Explore Single-Stage Stochastic Optimization examples

    main

    This chapter provides Pyomo implementations for various single-stage stochastic optimization problems. You can use these Jupyter notebooks to study modeling and implementation aspects of stochastic optimization through the following examples:

    • Markowitz portfolio with chance constraints: Modeling financial portfolios using chance constraints.
    • Optimal management of a pop-up shop: Managing inventory and operations for temporary retail locations.
    • Stock optimization for seafood distribution center: Optimizing stock levels in a distribution context.
    • Economic dispatch in energy systems using chance constraints: Applying chance constraints to energy system dispatch problems.
  10. Use functional programming patterns with Pyomo

    main
    The functional-programming-pyomo.ipynb notebook demonstrates how to apply functional programming styles to Pyomo. This is useful for integrating Pyomo into more complex optimization workflows or larger software projects where modularity and pure functions are preferred.
  11. Use `pyo` for the Pyomo namespace

    main

    To maintain consistency with Pyomo documentation and best practices, use pyo as the namespace for Pyomo imports. Avoid using from pyomo.environ import * as it is strongly discouraged. For teaching or presentations where a less verbose style is desired, explicitly import only the necessary objects.

    # Preferred convention
    import pyomo.environ as pyo
    
    # For teaching/presentations (explicit imports)
    from pyomo.environ import ConcreteModel, Var, Objective, maximize, SolverFactory
  12. Inspect solution sensitivity to uncertainty

    main

    When optimization parameters (like demand, travel times, or stock levels) are subject to estimation, measurement, or implementation errors, you can inspect how sensitive your solutions are to these changes. This chapter provides two practical examples of performing sensitivity and robustness analysis:

    1. Fleet assignment problem: Analyzing how changes in parameters affect fleet allocation.
    2. Robustness analysis of BIM production plan via simulations: Using simulations to test the robustness of a production plan against uncertainty.

    For proactive modeling, refer to the following chapters:

    • Robust Optimization: For formulating models that account for uncertainty in advance.
    • Stochastic Optimization: For modeling uncertainty using probability distributions.