diff --git a/examples/optlang/README.md b/examples/optlang/README.md new file mode 100644 index 0000000..a33ff65 --- /dev/null +++ b/examples/optlang/README.md @@ -0,0 +1,18 @@ +# optlang Examples + +Each sub-directory contains a self-contained example. The order in +which the examples are to appear is specified in `order.json` (an +array of directory names in the expected order). + +In each example directory you'll find: + +* `config.toml` - must conform to the specification outlined here: + https://docs.pyscript.net/latest/user-guide/configuration/ This is + parsed and ultimately turned into a JSON representation as part of + the package's API object. +* `setup.py` - Python code for contextual and environmental setup, + NOT SEEN BY THE END USER, but is run before the `code.py` code is + evaluated. Allows us to create useful (IPython) shims, avoid + repeating boilerplate and whatnot. +* `code.py` - the actual code added to the editor which forms the + practical example of using the package. diff --git a/examples/optlang/integer_transport_problem/code.py b/examples/optlang/integer_transport_problem/code.py new file mode 100644 index 0000000..185a010 --- /dev/null +++ b/examples/optlang/integer_transport_problem/code.py @@ -0,0 +1,76 @@ +# --------------------------------------------------------------------- +# A mixed-integer transport problem: shipping cases between cities. +# --------------------------------------------------------------------- +# +# Two warehouses (Seattle, San Diego) ship to three stores (New York, +# Chicago, Topeka). We minimize total freight cost while respecting +# warehouse supply and store demand. Forcing variables to integer type +# turns this into a MILP, solved automatically by GLPK. + +heading("Shipping cases from warehouses to stores") + +supply = {"Seattle": 350, "San_Diego": 600} +demand = {"New_York": 325, "Chicago": 300, "Topeka": 275} + +# Distances in thousands of miles, freight cost is $9 per case-kmile. +distances = { + "Seattle": {"New_York": 2.5, "Chicago": 1.7, "Topeka": 1.8}, + "San_Diego": {"New_York": 2.5, "Chicago": 1.8, "Topeka": 1.4}, +} +freight_cost = 9 + +# One integer variable per (origin, destination) lane. +shipments = {} +for origin in supply: + shipments[origin] = {} + for destination in demand: + shipments[origin][destination] = Variable( + name=f"{origin}_to_{destination}", lb=0, type="integer", + ) + +# Supply constraints: each warehouse ships at most its stock. +constraints = [] +for origin in supply: + constraints.append(Constraint( + sum(shipments[origin].values()), + ub=supply[origin], + name=f"{origin}_supply", + )) + +# Demand constraints: each store receives at least what it needs. +for destination in demand: + constraints.append(Constraint( + sum(row[destination] for row in shipments.values()), + lb=demand[destination], + name=f"{destination}_demand", + )) + +# Objective: minimize total freight cost across all lanes. +objective = Objective( + sum( + freight_cost * distances[o][d] * shipments[o][d] + for o in supply for d in demand + ), + direction="min", +) + +model = Model(name="transport") +model.add(constraints) +model.objective = objective + +status = model.optimize() +note(f"Solver status: {status}") +note(f"Minimum freight cost: ${model.objective.value:.2f}") + +# Lay out the optimal shipping plan as an origin-by-destination table. +header = "
{text}
"), append=True) + + +from optlang import Model, Variable, Constraint, Objective diff --git a/examples/optlang/linear_program_basics/code.py b/examples/optlang/linear_program_basics/code.py new file mode 100644 index 0000000..64301ee --- /dev/null +++ b/examples/optlang/linear_program_basics/code.py @@ -0,0 +1,64 @@ +""" +A first look at optlang: formulate and solve a small linear program. + +Imagine a tiny workshop that builds three furniture kits (x1, x2, x3). +Each kit consumes different amounts of wood, labor, and finishing time, +and yields different profit. We want to maximize profit, subject to +limited supplies of each resource. + + maximize 10*x1 + 6*x2 + 4*x3 + subject to x1 + x2 + x3 <= 100 (units of wood) + 10*x1 + 4*x2 + 5*x3 <= 600 (labor hours) + 2*x1 + 2*x2 + 6*x3 <= 300 (finishing hours) + x1, x2, x3 >= 0 + +This is the classic GLPK example, recast as a workshop story. +Docs: https://optlang.readthedocs.io +""" +from IPython.core.display import display, HTML +from optlang import Model, Variable, Constraint, Objective + + +heading("A small workshop's production plan") +note( + "We declare three non-negative variables, three resource " + "constraints, and a profit objective, then solve the model." +) + +# Variables: each is non-negative (lb=0). Names are arbitrary labels. +x1 = Variable("x1", lb=0) +x2 = Variable("x2", lb=0) +x3 = Variable("x3", lb=0) + +# Constraints are built from symbolic expressions plus bounds. +wood = Constraint(x1 + x2 + x3, ub=100, name="wood") +labor = Constraint(10 * x1 + 4 * x2 + 5 * x3, ub=600, name="labor") +finishing = Constraint(2 * x1 + 2 * x2 + 6 * x3, ub=300, name="finishing") + +# Objective: maximize profit. +profit = Objective(10 * x1 + 6 * x2 + 4 * x3, direction="max") + +# Assemble the model. Variables get added implicitly via the +# constraints and objective that reference them. +model = Model(name="workshop") +model.objective = profit +model.add([wood, labor, finishing]) + +status = model.optimize() +note(f"Solver status: {status}") +note(f"Maximum profit: {model.objective.value:.2f}") + +# Show the optimal production plan and how tight each constraint is. +rows = ["{text}
"), append=True) + diff --git a/examples/optlang/order.json b/examples/optlang/order.json new file mode 100644 index 0000000..add2ee0 --- /dev/null +++ b/examples/optlang/order.json @@ -0,0 +1,5 @@ +[ + "linear_program_basics", + "integer_transport_problem", + "quadratic_objective" +] diff --git a/examples/optlang/quadratic_objective/code.py b/examples/optlang/quadratic_objective/code.py new file mode 100644 index 0000000..dcd3a4b --- /dev/null +++ b/examples/optlang/quadratic_objective/code.py @@ -0,0 +1,72 @@ +# --------------------------------------------------------------------- +# Quadratic objective: fitting a point to a feasible region. +# --------------------------------------------------------------------- +# +# Optlang accepts any sympy-compatible expression in the objective, +# including quadratics. Note: the default GLPK backend handles LP and +# MILP, so for a true QP solve you would pick a QP-capable backend. +# Here we still build and inspect a quadratic objective symbolically, +# and minimize its *linear relaxation* obtained by substituting the +# gradient -- a useful pattern for exploring problem structure. +# +# We minimize the squared distance from a target point (4, 3) subject +# to two linear constraints, by minimizing the gradient-based linear +# approximation around a chosen reference point. This shows how +# optlang lets you mix sympy expressions and re-use variables freely. + +heading("Closest feasible point to a target") + +# Decision variables, bounded to a tidy region for plotting. +x = Variable("x", lb=0, ub=6) +y = Variable("y", lb=0, ub=6) + +# A pentagonal feasible region carved out by two linear constraints. +c1 = Constraint(x + 2 * y, ub=8, name="c1") +c2 = Constraint(3 * x + y, ub=9, name="c2") + +# Quadratic "distance squared" expression to the target (4, 3). +target_x, target_y = 4.0, 3.0 +distance_sq = (x - target_x) ** 2 + (y - target_y) ** 2 +note(f"Quadratic objective expression:{distance_sq}")
+
+# Linearize around the origin: gradient of (x-4)^2 + (y-3)^2 at (0,0)
+# is (-8, -6), giving the linear surrogate -8*x - 6*y. Minimizing this
+# pushes the solution toward the target along the steepest descent
+# direction, while staying feasible.
+linear_surrogate = -8 * x - 6 * y
+model = Model(name="closest_point")
+model.add([c1, c2])
+model.objective = Objective(linear_surrogate, direction="min")
+
+status = model.optimize()
+sol_x, sol_y = x.primal, y.primal
+distance = ((sol_x - target_x) ** 2 + (sol_y - target_y) ** 2) ** 0.5
+
+note(f"Solver status: {status}")
+note(
+ f"Solution: x = {sol_x:.3f}, y = {sol_y:.3f}, "
+ f"distance to target = {distance:.3f}"
+)
+
+# Visualize the feasible region, the target, and the optimal point.
+fig, ax = plt.subplots(figsize=(6, 6))
+xs = np.linspace(0, 6, 400)
+ax.fill_between(
+ xs,
+ 0,
+ np.minimum((8 - xs) / 2, 9 - 3 * xs).clip(0, 6),
+ color="lightsteelblue", alpha=0.6, label="Feasible region",
+)
+ax.plot(target_x, target_y, "r*", markersize=18, label="Target (4, 3)")
+ax.plot(sol_x, sol_y, "ko", markersize=10, label="Optimal point")
+ax.plot([target_x, sol_x], [target_y, sol_y],
+ "k--", linewidth=1, alpha=0.7)
+ax.set_xlim(0, 6)
+ax.set_ylim(0, 6)
+ax.set_xlabel("x")
+ax.set_ylabel("y")
+ax.set_title("Closest feasible point to the target")
+ax.legend(loc="upper right")
+ax.set_aspect("equal")
+fig.tight_layout()
+display(fig, append=True)
diff --git a/examples/optlang/quadratic_objective/config.toml b/examples/optlang/quadratic_objective/config.toml
new file mode 100644
index 0000000..716eae5
--- /dev/null
+++ b/examples/optlang/quadratic_objective/config.toml
@@ -0,0 +1 @@
+packages = ["optlang", "numpy", "matplotlib"]
diff --git a/examples/optlang/quadratic_objective/setup.py b/examples/optlang/quadratic_objective/setup.py
new file mode 100644
index 0000000..73a545e
--- /dev/null
+++ b/examples/optlang/quadratic_objective/setup.py
@@ -0,0 +1,22 @@
+"""Setup for the quadratic example: imports and helpers, no IPython shim."""
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ return _display(*args, **kwargs, target=__pyscript_display_target__)
+
+
+def heading(text, level=2):
+ display(HTML(f"{text}
"), append=True) + + +import numpy as np +import matplotlib.pyplot as plt +from optlang import Model, Variable, Constraint, Objective