Note

You can download this example as a Jupyter notebook or start it in interactive mode.

Solve on OETC (OET Cloud)#

This example demonstrates how to use linopy with OETC (OET Cloud) for cloud-based optimization solving. OETC is a cloud platform that provides scalable computing resources for optimization problems.

What you need to run this example:#

  • A working installation of the required packages:

    • pip install google-cloud-storage requests

  • An OETC account with valid credentials (email and password)

  • Access to OETC authentication and orchestrator servers

How OETC Cloud Solving Works#

The OETC integration follows this workflow:

  1. Model Creation: Define your optimization model locally using linopy

  2. Authentication: Sign in to the OETC platform using your credentials

  3. File Upload: Compress and upload your model to Google Cloud Storage

  4. Job Submission: Submit a compute job to the OETC orchestrator

  5. Job Monitoring: Wait for job completion with automatic status polling

  6. Solution Download: Download and decompress the solved model

  7. Local Integration: Load the solution back into your local model

All of these steps are handled automatically by linopy’s OetcHandler.

Note: This notebook requires Google Cloud credentials and access to the OETC platform. It is not executed during the documentation build, so no cell outputs are shown. To run it yourself, install the linopy[oetc] extra and configure your credentials.

Create a Model#

First, let’s create an optimization model that we want to solve on OETC:

[ ]:
from numpy import arange
from xarray import DataArray

from linopy import Model

# Create a medium-sized optimization problem
N = 50
m = Model()

# Define decision variables with coordinates
coords = [arange(N), arange(N)]
x = m.add_variables(coords=coords, name="x", lower=0)
y = m.add_variables(coords=coords, name="y", lower=0)

# Add constraints
m.add_constraints(x - y >= DataArray(arange(N)), name="constraint1")
m.add_constraints(x + y >= DataArray(arange(N) * 0.5), name="constraint2")
m.add_constraints(x <= DataArray(arange(N) + 10), name="upper_bounds")

# Set objective function
m.add_objective((2 * x + y).sum())

print(
    f"Model created with {len(m.variables)} variable groups and {len(m.constraints)} constraint groups"
)
m

Configure OETC Settings#

There are two ways to configure OETC settings:

  1. Manual construction — build OetcCredentials and OetcSettings explicitly

  2. ``OetcSettings.from_env()`` — resolve credentials and options from environment variables

Option 1: Manual Construction#

[ ]:
# Configure your OETC credentials
# IMPORTANT: Never hardcode credentials in production code!
# Use environment variables or secure credential management
import os

from linopy.remote.oetc import (
    ComputeProvider,
    OetcCredentials,
    OetcHandler,
    OetcSettings,
)

credentials = OetcCredentials(
    email=os.getenv("OETC_EMAIL", "your-email@example.com"),
    password=os.getenv("OETC_PASSWORD", "your-password"),
)

# Configure OETC settings
settings = OetcSettings(
    credentials=credentials,
    name="linopy-example-job",
    authentication_server_url="https://auth.oetcloud.com",  # Replace with actual URL
    orchestrator_server_url="https://orchestrator.oetcloud.com",  # Replace with actual URL
    compute_provider=ComputeProvider.GCP,
    cpu_cores=4,  # Number of CPU cores to allocate
    disk_space_gb=20,  # Disk space in GB
    delete_worker_on_error=False,  # Keep worker for debugging if job fails
)

print("OETC settings configured successfully")
print(f"Solver: {settings.solver}")
print(f"CPU cores: {settings.cpu_cores}")
print(f"Disk space: {settings.disk_space_gb} GB")

Option 2: Create Settings from Environment Variables#

OetcSettings.from_env() reads configuration from environment variables, with optional keyword overrides. This is the recommended approach for CI/CD pipelines and production deployments.

Environment Variable

Required

Description

OETC_EMAIL

Yes

Account email

OETC_PASSWORD

Yes

Account password

OETC_NAME

Yes

Job name

OETC_AUTH_URL

Yes

Authentication server URL

OETC_ORCHESTRATOR_URL

Yes

Orchestrator server URL

OETC_CPU_CORES

No

CPU cores (default: 2)

OETC_DISK_SPACE_GB

No

Disk space in GB (default: 10)

OETC_DELETE_WORKER_ON_ERROR

No

Delete worker on error (default: false)

Keyword arguments take precedence over environment variables.

[ ]:
# Create settings from environment variables
# All required env vars must be set: OETC_EMAIL, OETC_PASSWORD,
# OETC_NAME, OETC_AUTH_URL, OETC_ORCHESTRATOR_URL
settings = OetcSettings.from_env()

# Or override specific values via keyword arguments
settings = OetcSettings.from_env(
    cpu_cores=8,
    disk_space_gb=50,
)

Initialize OETC Handler#

The OetcHandler manages the entire cloud solving process:

[ ]:
# Initialize the OETC handler
# This will authenticate with OETC and fetch cloud provider credentials
oetc_handler = OetcHandler(settings)

print("OETC handler initialized successfully")
print(f"Authentication token expires at: {oetc_handler.jwt.expires_at}")

Solve the Model on OETC#

Now we can solve our model on the OETC cloud platform. The OetcHandler is passed to the model’s solve() method:

[ ]:
# Solve the model on OETC
# This will upload the model, submit a job, wait for completion, and download the solution
import time

print("Starting cloud solving process...")
start_time = time.time()

try:
    status, termination_condition = m.solve(remote=oetc_handler, solver_name="highs")

    end_time = time.time()
    total_time = end_time - start_time

    print(f"\nSolving completed in {total_time:.2f} seconds")
    print(f"Status: {status}")
    print(f"Termination condition: {termination_condition}")
    print(f"Objective value: {m.objective.value:.4f}")

except Exception as e:
    print(f"Error during solving: {e}")
    raise

Examine the Solution#

Let’s examine the solution returned from OETC:

[ ]:
# Display solution summary
print(f"Model status: {m.status}")
print(f"Objective value: {m.objective.value}")
print(f"Number of variables: {m.solution.sizes}")

# Show a subset of the solution
print("\nSample of solution values:")
print("x values (first 5x5):")
print(m.solution["x"].isel(dim_0=slice(0, 5), dim_1=slice(0, 5)).values)

print("\ny values (first 5x5):")
print(m.solution["y"].isel(dim_0=slice(0, 5), dim_1=slice(0, 5)).values)

Advanced OETC Configuration#

Solver Options#

Solver name and options can be configured at two levels:

  1. Settings level — defaults stored in OetcSettings.solver and OetcSettings.solver_options

  2. Call level — passed via m.solve(solver_name=..., **solver_options)

Call-level options override settings-level options. The two dicts are merged (call-time takes precedence), and the original settings are never mutated.

[ ]:
# Settings-level defaults
advanced_settings = OetcSettings(
    credentials=credentials,
    name="advanced-linopy-job",
    authentication_server_url="https://auth.oetcloud.com",
    orchestrator_server_url="https://orchestrator.oetcloud.com",
    solver="gurobi",
    solver_options={
        "TimeLimit": 600,
        "MIPGap": 0.01,
    },
    cpu_cores=8,
    disk_space_gb=50,
)

advanced_handler = OetcHandler(advanced_settings)

# Call-level overrides: solver_name and solver_options are forwarded
# to OETC and merged with the settings defaults.
# Here MIPGap from settings (0.01) is kept, TimeLimit is overridden to 300.
status, condition = m.solve(
    remote=advanced_handler,
    solver_name="gurobi",
    TimeLimit=300,
    Threads=4,
)

Error Handling and Debugging#

When working with cloud solving, it’s important to handle potential errors gracefully:

[ ]:
def solve_with_error_handling(model, oetc_handler, max_retries=3):
    """Solve model with error handling and retries"""

    for attempt in range(max_retries):
        try:
            print(f"Solving attempt {attempt + 1}/{max_retries}...")
            status, termination = model.solve(remote=oetc_handler)

            if status == "ok":
                print("Solving successful!")
                return status, termination
            else:
                print(f"Solving returned status: {status}")

        except Exception as e:
            print(f"Attempt {attempt + 1} failed: {e}")

            if attempt < max_retries - 1:
                print("Retrying in 30 seconds...")
                time.sleep(30)
            else:
                print("All attempts failed")
                raise

    return None, None


# Example usage (commented out to avoid actual execution)
# status, termination = solve_with_error_handling(m, oetc_handler)

Security Best Practices#

When using OETC in production:

  1. Never hardcode credentials: Use environment variables or secure credential stores

  2. Use token expiration: The OETC handler automatically manages token expiration

  3. Validate inputs: Ensure your model data doesn’t contain sensitive information

  4. Monitor costs: Cloud computing resources have associated costs

  5. Clean up resources: Set delete_worker_on_error=True for automatic cleanup

Comparison with SSH Remote Solving#

Feature

OETC Cloud

SSH Remote

Setup

Account registration

Server access required

Scalability

Auto-scaling

Fixed server resources

Maintenance

Managed service

Self-managed

Cost

Pay-per-use

Infrastructure costs

Security

Enterprise-grade

Self-managed

Solver Licenses

Included

User-provided

Choose OETC for: - Large-scale problems requiring significant compute resources - Temporary or intermittent optimization needs - Teams without dedicated infrastructure - Access to premium solvers without license management

Choose SSH remote for: - Existing infrastructure with optimization solvers - Strict data governance requirements - Consistent, long-running optimization workloads - Full control over the solving environment