Control Flow Structures#

GAMSPy provides programming flow control features that map directly to native GAMS flow control statements. Because Python’s native execution model evaluates eagerly, writing standard Python for loops to iterate over GAMS data can introduce massive performance overhead due to constant communication between Python and GAMS.

To solve this, GAMSPy uses specialized constructs like context managers (with statements) to generate the underlying GAMS execution statements dynamically, executing the entire block directly within the highly optimized GAMS engine.

Vectorized Assignment vs. The Loop Statement#

Before using a loop, consider whether your logic can be expressed as a standard parallel assignment. For example, simple recursive or arithmetic updates do not require a loop:

# BAD: Unnecessary loop
with gp.Loop(t):
    pop[t + 1] = pop[t] + growth[t]

# GOOD: Fast, native parallel assignment
pop[t + 1] = pop[t] + growth[t]

The Loop context manager should be reserved for cases where parallel assignments are not sufficient. This includes complex iterative calculations, dynamic algorithmic heuristics, and modifying models to solve them repeatedly.

Iteratively Solving Models#

A common use case in operations research is modifying model parameters and solving the model multiple times within a loop. Executing this via a standard Python for loop is notoriously slow. By using Loop, you push the entire iterative solve process to the GAMS backend:

import gamspy as gp

# ... (Assume sets i, j, variables x, and equations are already defined) ...

transport = gp.Model(
    m,
    name="transport",
    equations=m.getEquations(),
    problem="LP",
    sense=gp.Sense.MIN,
    objective=gp.Sum((i, j), c[i, j] * x[i, j]),
)

k = gp.Set(m, domain=[i, j])
k[i, j] = True

# Slow iteration in Python
for ival in i.toList():
    for jval in j.toList():
        transport.solve()
        c[ival, jval] = c[ival, jval] * 1.1

# Fast iteration directly inside GAMS
with gp.Loop(k):
    transport.solve()
    c[i, j] = c[i, j] * 1.1  # Update cost dynamically for the next solve

Dollar Conditions in Loops#

The domain of the loop can be restricted by a logical condition using the .where attribute. This acts identically to a dollar condition on a loop in GAMS, allowing you to filter the iteration space efficiently.

i = gp.Set(m, records=[f"i{idx}" for idx in range(1, 4)])
j = gp.Set(m, records=[f"j{idx}" for idx in range(1, 6)])
q = gp.Parameter(m, domain=[i, j], records=[("i1", "j1", 1), ("i1", "j2", 3)])
x = gp.Parameter(m, records=1)

# Only iterate over combinations where q[i, j] is strictly positive
with gp.Loop(gp.Domain(i, j).where[q[i, j] > 0]):
    x[...] = x[...] + q[i, j]

The For Statement#

The For class maps to the GAMS for statement. While Loop is used to iterate over members of a set, For allows you to iterate over a range of numerical values, incrementing or decrementing a scalar parameter at each step. It is useful for iterative algorithmic calculations that require a numerical counter.

import gamspy as gp

m = gp.Container()
i = gp.Parameter(m) # Scalar parameter acting as the loop counter
cnt = gp.Parameter(m, records=0)

# Simple iteration over a numerical range (1 to 10)
with gp.For(i, 1, 10):
    cnt[...] += i

You can also specify a custom step size. The step must be positive. To step downwards, one can specify the direction as follows:

.. code-block:: python

x = gp.Parameter(m, records=10)

# Iterating backwards from 10 down to 1 with a step size of -2 with gp.For(i, 10, 1, 2, direction=”downto”):

x[…] = x[…] - 2

You can also use other Parameters or Expressions to define the start, end, and step boundaries of the loop dynamically.

The If Statement#

The If class maps directly to the GAMS if statement. It allows you to branch conditionally around a group of execution statements within control flow constructs like loops. Currently, its use is restricted within a Loop statement. Like Loop, it is implemented as a Python context manager and takes a GAMSPy logical condition as its argument:

import gamspy as gp

m = gp.Container()
i = gp.Set(m, records=[f"i{idx}" for idx in range(1, 11)])
cnt = gp.Parameter(m, records=0)

with gp.Loop(i):
    # Only execute the following block if the condition is met
    with gp.If(gp.Ord(i) > 5):
        cnt[...] += 1

Break and Continue#

When iterating using a Loop or For, you can capture the loop instance using the as keyword (e.g., with gp.Loop(i) as loop: or with gp.For(i, 1, 10) as loop:). This instance provides access to the Break and Continue properties, which map to the GAMS break and continue statements.

  • Continue: Skips the remaining statements in the current iteration and proceeds to the next.

  • Break: Terminates the execution of the current loop prematurely.

Using Continue#

You can combine If and Continue to skip specific iterations dynamically.

with gp.Loop(i) as loop:
    # Skip even numbers
    with gp.If(gp.math.mod(gp.Ord(i), 2) == 0):
        loop.Continue

    cnt[...] += 1

Using Break#

You can use Break to exit a loop early, such as when a heuristic search finds an acceptable candidate. Note that you can only break out of the innermost loop currently executing.

j = gp.Set(m, records=[f"j{idx}" for idx in range(1, 11)])

with gp.Loop(i) as loop_i:
    with gp.Loop(j) as loop_j:

        with gp.If(j.sameAs("j5")):
            loop_j.Break  # Breaks the inner loop (j)

        cnt[...] += 1

    with gp.If(i.sameAs("i5")):
        loop_i.Break  # Breaks the outer loop (i)