Skip to content

Replication Guide

Prerequisites

System Requirements

  • Python 3.11+
  • ~8 GB RAM (for JAX compilation)
  • ~2 GB disk space (for results)

Installation

# Clone the repository
git clone https://github.com/kilojoules/cluster-tradeoffs.git
cd cluster-tradeoffs

# Install dependencies with pixi
curl -fsSL https://pixi.sh/install.sh | bash
pixi install

Quick Start

Run a Single Configuration

# Single direction, 5 blobs, 5 starts (fastest, ~10 minutes)
pixi run python scripts/run_regret_discovery.py \
    --wind-rose=single \
    --n-blobs=5 \
    --n-starts=5

Run Wind Rose Comparison

# Compare all wind rose types (5 blobs each, ~1 hour)
pixi run python scripts/run_regret_discovery.py \
    --wind-rose=comparison \
    --n-blobs=5 \
    --n-starts=5

Full Analysis (Paper Results)

# Full analysis (20 blobs, 20 starts, ~4-6 hours)
pixi run python scripts/run_regret_discovery.py \
    --wind-rose=comparison \
    --n-blobs=20 \
    --n-starts=20 \
    --output-dir=analysis/wind_rose_comparison_full

Danish Energy Island (DEI) Case Study

# Single-neighbor analysis with actual DEI geometry
pixi run python scripts/run_dei_single_neighbor.py \
    --n-starts=5 \
    --max-iter=500

# With more optimization effort for robust results
pixi run python scripts/run_dei_single_neighbor.py \
    --n-starts=10 \
    --max-iter=1000

# With TurboPark wake model
pixi run python scripts/run_dei_single_neighbor.py \
    --wake-model=turbopark \
    --ti=0.06

The DEI analysis uses actual wind data from OMAE_neighbors/energy_island_10y_daily_av_wind.csv and precomputed neighbor layouts from OMAE_neighbors/re_precomputed_layouts.h5.

Command Reference

Main Script: run_regret_discovery.py

usage: run_regret_discovery.py [-h] [--wind-rose {single,uniform,von_mises,bimodal,comparison}]
                               [--n-directions N] [--dominant-dir DEG] [--concentration K]
                               [--secondary-dir DEG] [--mean-ws M/S] [--n-blobs N]
                               [--n-starts N] [--output-dir DIR]

Options:
  --wind-rose, -w       Wind rose type (default: single)
                        - single: unidirectional at dominant-dir
                        - uniform: omnidirectional with n-directions
                        - von_mises: Von Mises distribution
                        - bimodal: two peaks
                        - comparison: run all types

  --n-directions, -d    Number of wind directions (default: 24)
  --dominant-dir        Primary wind direction in degrees (default: 270)
  --concentration, -k   Von Mises kappa parameter (default: 2.0)
  --secondary-dir       Secondary direction for bimodal (default: 90)
  --mean-ws             Mean wind speed in m/s (default: 9.0)
  --n-blobs             Number of blob configurations (default: 10)
  --n-starts            Optimization starts per strategy (default: 10)
  --output-dir, -o      Output directory (auto-generated if not specified)
# Run gradient tests (validates IFT machinery, ~40 seconds)
pixi run python -m pytest tests/test_ift_gradients.py -v

# Component-level cost benchmarks (~5 minutes)
pixi run python scripts/explore_cost_tradeoffs.py

# Multistart strategy benchmark (~10 minutes)
pixi run python scripts/benchmark_multistart.py

# Animated bilevel prototype (generates MP4s, ~3 minutes)
pixi run python scripts/prototype_bilevel_animated.py

# Inner loop animations (first/last outer iteration, ~3 minutes)
pixi run python scripts/animate_inner_loop.py

Convergence Study: run_convergence_study.py

# Verify convergence of regret estimates
pixi run python scripts/run_convergence_study.py --n-starts-max=40

DEI Single-Neighbor Analysis: run_dei_single_neighbor.py

usage: run_dei_single_neighbor.py [-h] [--wake-model {bastankhah,turbopark}]
                                   [--ti TI] [--n-starts N] [--max-iter N]
                                   [--output-dir DIR]

Options:
  --wake-model          Wake model: bastankhah or turbopark (default: bastankhah)
  --ti                  Ambient turbulence intensity for TurboPark (default: 0.06)
  --n-starts            Multi-start optimization runs per strategy (default: 5)
  --max-iter            Maximum iterations per optimization run (default: 500)
  --output-dir, -o      Output directory (default: analysis/dei_single_neighbor)

This script tests each of the 9 DEI neighbor farms individually to identify which neighbors create design regret.

Output Structure

analysis/
├── wind_rose_comparison_full/
│   ├── comparison_summary.json      # Summary statistics
│   ├── pareto_comparison_all.png    # Combined Pareto plot
│   ├── wind_rose_comparison.png     # Bar chart comparison
│   │
│   ├── single_270deg/
│   │   ├── results.json             # Full optimization results
│   │   ├── wind_rose_config.json    # Wind rose parameters
│   │   ├── pareto_frontier.png      # Pareto frontier for blob 0
│   │   ├── blob_0.png               # Individual blob result
│   │   ├── blob_1.png
│   │   └── ...
│   │
│   ├── uniform_24dir/
│   │   └── ...
│   │
│   └── von_mises_270deg_k2.0/
│       └── ...
│
└── dei_single_neighbor/
    ├── dei_single_neighbor_bastankhah.json   # Regret by neighbor farm
    └── dei_single_neighbor_bastankhah.png    # Polar plot of regret

Results JSON Format

{
  "blob_seed": 3,
  "control_points": [[x1, y1], [x2, y2], ...],
  "all_layouts": [
    {
      "strategy": "liberal",
      "x": [x1, x2, ...],
      "y": [y1, y2, ...],
      "aep_absent": 1168.0,
      "aep_present": 1011.0
    },
    ...
  ],
  "global_best_aep_absent": 1168.0,
  "global_best_aep_present": 1072.0,
  "min_liberal_regret": 0.0,
  "min_conservative_regret": 0.0
}

Customization

Changing Farm Size

Edit scripts/run_regret_discovery.py:

D = 200.0           # Rotor diameter [m]
target_size = 16*D  # Farm size [m]
min_spacing = 4*D   # Minimum turbine spacing [m]
n_target = 16       # Number of turbines

Changing Turbine

Modify the create_turbine() function:

def create_turbine(rotor_diameter=200.0):
    ws = jnp.array([0.0, 4.0, 10.0, 15.0, 25.0])
    power = jnp.array([0.0, 0.0, 10000.0, 10000.0, 0.0])  # kW
    ct = jnp.array([0.0, 0.8, 0.8, 0.4, 0.0])
    return Turbine(
        rotor_diameter=rotor_diameter,
        hub_height=120.0,
        power_curve=Curve(ws=ws, values=power),
        ct_curve=Curve(ws=ws, values=ct),
    )

Adding New Wind Rose Types

Add to scripts/run_regret_discovery.py:

def _generate_custom_rose(config):
    """Custom wind rose implementation."""
    wd = jnp.linspace(0, 360, config.n_directions, endpoint=False)
    # Custom weight calculation
    weights = ...
    ws = jnp.full_like(wd, config.mean_ws)
    return wd, ws, weights

Troubleshooting

JAX Compilation Warnings

UserWarning: Error reading persistent compilation cache entry

These are harmless cache corruption warnings. JAX will recompile as needed.

Memory Issues

If you encounter OOM errors:

  1. Reduce n_directions (e.g., 12 instead of 24)
  2. Reduce n_starts (e.g., 10 instead of 20)
  3. Run wind rose types sequentially instead of comparison mode

Slow Performance

  • First run is slow due to JAX compilation (~5 min)
  • Subsequent runs use cached compilation
  • GPU acceleration available if JAX GPU is installed

Citation

If you use this code, please cite:

@software{cluster_tradeoffs,
  author = {Quick, Julian},
  title = {Wind Farm Cluster Tradeoffs},
  year = {2025},
  url = {https://github.com/kilojoules/cluster-tradeoffs}
}