Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions .github/workflows/julia-tests.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: FLOWFarm integration tests (Julia)

on:
push:
paths:
- "ard/farm_aero/flowfarm/**"
- "test/flowfarm/**"
pull_request:
paths:
- "ard/farm_aero/flowfarm/**"
- "test/flowfarm/**"
workflow_dispatch: # allow manual runs from the Actions UI

jobs:

test-julia:
name: Run FLOWFarm integration tests
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Set up Julia
uses: julia-actions/setup-julia@v2
with:
version: "1.10"

- name: Cache Julia packages
uses: julia-actions/cache@v2

- name: Install Ard with FLOWFarm extras
run: pip install ".[dev,flowfarm]"

- name: Pre-instantiate Julia environment
run: |
julia -e "using Pkg; Pkg.Registry.add(\"General\"); Pkg.Registry.update()"
julia --project=ard/farm_aero/flowfarm/julia_env -e "using Pkg; Pkg.instantiate(); Pkg.precompile()"

- name: Run FLOWFarm integration tests
run: |
pytest -m julia test/flowfarm/integration \
--cov=ard \
--cov-report=term-missing \
-v
15 changes: 14 additions & 1 deletion .github/workflows/python-tests-consolidated.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,20 @@ jobs:
pip install .[dev]
- name: Run unit tests with coverage
run: |
pytest --cov=ard --cov-fail-under=80 test/ard/unit
export MPLBACKEND=Agg
pytest --cov=ard --cov-fail-under=80 test/ard/unit -m "not julia"

# If no julia-marked tests are present in test/ard/unit, pytest exits 5.
# Treat that as success so CI remains stable across marker rollout.
set +e
pytest --cov=ard --cov-append test/ard/unit -m "julia"
rc=$?
set -e
if [ "$rc" -ne 0 ] && [ "$rc" -ne 5 ]; then
exit "$rc"
fi

pytest --cov=ard --cov-append test/flowfarm/unit -m "not julia"

test-system:
name: Run system tests
Expand Down
7 changes: 6 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@

### ARD DEVELOPMENT IGNORES
### ARD DEVELOPMENT IGNORES

.vscode
case_files
ard_prob_out

### JULIA
# Manifest.toml is user-generated (depends on local Julia version).
# Project.toml is committed; Manifest.toml is rebuilt automatically on first use.
ard/farm_aero/flowfarm/julia_env/Manifest.toml

### MACOS DEFAULT IGNORES

.DS_Store
Expand Down
1 change: 1 addition & 0 deletions ard/farm_aero/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from . import floris
from . import flowfarm
from . import placeholder
from . import templates
147 changes: 147 additions & 0 deletions ard/farm_aero/flowfarm/README.md
Comment thread
BTV25 marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
# FLOWFarm Integration in Ard

This folder contains Ard's Python-Julia integration utilities for FLOWFarm.

## Julia setup (required before first use)

FLOWFarm runs inside Julia via [JuliaCall](https://juliapy.github.io/PythonCall.jl/stable/). You need Julia installed before running any FLOWFarm components. Users who do not use FLOWFarm do not need Julia at all — it is loaded lazily only when a FLOWFarm component is initialized.

### 1. Create a conda environment with Python + juliaup (recommended)

```bash
conda create --name ard-FLOWFarm python=3.13 juliaup
conda activate ard-FLOWFarm
```

Set both Julia depots to a path inside the active conda env:

```bash
conda env config vars set JULIA_DEPOT_PATH=${CONDA_PREFIX}/.julia
conda env config vars set JULIAUP_DEPOT_PATH=${CONDA_PREFIX}/.julia
conda deactivate
conda activate ard-FLOWFarm
```

Install Julia (1.x) using juliaup from that environment:

```bash
juliaup add release
juliaup default release
julia --version
```

Then install Ard (including FLOWFarm dependencies):

```bash
pip install -e ".[dev,flowfarm]"
```

### 2. Pre-generate the Julia environment (optional)

On first use Ard will resolve and instantiate the Julia environment automatically. If you prefer to do this ahead of time — for example on an HPC cluster node without internet access at runtime — run once from your terminal:

```bash
julia --project="<path-to-ard>/ard/farm_aero/flowfarm/julia_env" -e "using Pkg; Pkg.resolve(); Pkg.instantiate()"
```

Replace `<path-to-ard>` with the absolute path to the `Ard` directory. This downloads FLOWFarm and its dependencies. It may take several minutes on first run.

## What this integration does

- Boots Julia through JuliaCall.
- Activates Ard's local Julia environment (`julia_env`).
- Loads FLOWFarm and builds farm and sparse structs for Ard components.
- Exposes helper functions used by the component wrapper in `ard/farm_aero/flowfarm/component.py`.

## Threading and parallelism

### OpenMDAO does not multithread

OpenMDAO is single-threaded by design. Its solver loops (Newton, Gauss-Seidel, NLBGS, etc.) are serial. The only parallelism OpenMDAO exposes is MPI-based **process** parallelism via `ParallelGroup`, which spawns separate processes — not threads. This means OpenMDAO will never call the FLOWFarm component from multiple threads simultaneously, so there is no concurrency risk from the OpenMDAO layer.

### Julia internal threading (FLOWFarm parallelism)

Threading in this integration refers to Julia's own thread pool, which FLOWFarm can use internally to parallelize wake calculations across turbines. This is separate from and independent of OpenMDAO.

Julia's thread count is fixed at startup and cannot be changed at runtime. Configure it **before** importing Ard or JuliaCall:

```python
import os
os.environ["PYTHON_JULIACALL_THREADS"] = "4" # or "auto" to use all cores
os.environ["PYTHON_JULIACALL_HANDLE_SIGNALS"] = "yes"
```

For threaded runs on shared-memory machines, also consider limiting BLAS and OpenMP thread pools to avoid nested oversubscription (Julia threads × BLAS threads × cores):

```python
os.environ["OPENBLAS_NUM_THREADS"] = "1"
os.environ["OMP_NUM_THREADS"] = "1"
```

These must be set before the first `import ard` call in your script or notebook.

### Why a pure Julia callback

The FLOWFarm update callback in Ard is implemented entirely in Julia (not as a Python callable passed into Julia). This is required for thread safety: JuliaCall does not support calling back into Python from Julia threads other than the main thread. Using a pure Julia callback avoids this restriction and allows FLOWFarm to use all available Julia threads.

## Tolerance behavior

- FLOWFarm sparse-structure tolerance uses `modeling_options.flowfarm.tolerance`.
- If not provided, the default is `1e-16`.

Example:

```yaml
modeling_options:
flowfarm:
tolerance: 1.0e-16
```

## Supported FLOWFarm names

Use the exact FLOWFarm constructor and model names expected by the integration.

### Turbine model constructors

- `PowerModelCpPoints`
- `PowerModelConstantCp`
- `ThrustModelCtPoints`
- `ThrustModelConstantCt`

### Wake model options

- `wake_deficit_model`: `JensenTopHat`, `JensenCosine`, `MultiZone`, `GaussOriginal`, `GaussYaw`, `GaussYawVariableSpread`, `GaussSimple`, `CumulativeCurl`, `NoWakeDeficit`
- `wake_deflection_model`: `NoYawDeflection`, `GaussYawDeflection`, `GaussYawVariableSpreadDeflection`, `JiminezYawDeflection`, `MultizoneDeflection`
- `wake_combination_model`: `LinearFreestreamSuperposition`, `SumOfSquaresFreestreamSuperposition`, `SumOfSquaresLocalVelocitySuperposition`, `LinearLocalVelocitySuperposition`
- `local_turbulence_model`: `LocalTIModelNoLocalTI`, `LocalTIModelMaxTI`, `LocalTIModelGaussTI`

## Key files

- `_jl_bootstrap.py`: Julia runtime bootstrap and env activation helpers.
- `flowfarm_model.py`: FLOWFarm model-construction utilities and option validation.
- `component.py`: OpenMDAO component wrapper that uses this integration.

## Troubleshooting

### Julia manifest warnings on first run

If you see warnings like "manifest resolved with a different julia version" or "project dependencies have changed since the manifest was last resolved", it means the local `Manifest.toml` is missing or stale. Ard will attempt to rebuild it automatically. If it does not, run:

```bash
julia --project="<path-to-ard>/ard/farm_aero/flowfarm/julia_env" -e "using Pkg; Pkg.resolve(); Pkg.instantiate()"
```

Then restart your Jupyter kernel. The `Manifest.toml` is not committed to the repository — it is always generated locally for your Julia version.


This comes from your **global** Julia environment, not Ard's. JuliaCall triggers the IPython/Jupyter juliacall extension on import.

### Wrong Julia version being used

If Julia 1.11+ is picked up instead of 1.10, check your `PATH`. `juliaup default 1.10` sets the default for commands run via juliaup, but if `/opt/homebrew/bin/julia` or another system Julia takes precedence in your shell, JuliaCall may use that instead.

### Kernel/process crash when threads > 1

- Ensure pure Julia callback path is active (current Ard default).
- Ensure thread env vars are set before importing Ard.
- Start with `PYTHON_JULIACALL_THREADS=1`, then increase.
10 changes: 10 additions & 0 deletions ard/farm_aero/flowfarm/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from .component import FLOWFarmAEP, FLOWFarmBatchPower, FLOWFarmComponent
from ._jl_bootstrap import ensure_flowfarm_loaded, get_julia_runtime

__all__ = [
"FLOWFarmAEP",
"FLOWFarmBatchPower",
"FLOWFarmComponent",
"ensure_flowfarm_loaded",
"get_julia_runtime",
]
34 changes: 34 additions & 0 deletions ard/farm_aero/flowfarm/_jl_bootstrap.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# ard/farm_aero/flowfarm/_jl_bootstrap.py
from __future__ import annotations
import pathlib

_jl_runtime = None
_flowfarm_env_initialized = False


def get_julia_runtime():
"""Return (Main, Pkg) from JuliaCall with Ard-safe bootstrap behavior."""
global _jl_runtime
if _jl_runtime is not None:
return _jl_runtime

from juliacall import Main as jl_main
from juliacall import Pkg as jl_pkg

_jl_runtime = (jl_main, jl_pkg)
return _jl_runtime


def ensure_flowfarm_loaded():
"""Activate Ard Julia env and load FLOWFarm in Julia Main."""
global _flowfarm_env_initialized
jl_main, jl_pkg = get_julia_runtime()
if not _flowfarm_env_initialized:
env_dir = pathlib.Path(__file__).parent / "julia_env"
jl_pkg.activate(str(env_dir))
jl_pkg.instantiate()
_flowfarm_env_initialized = True

if "FLOWFarm" not in dir(jl_main):
jl_main.seval("using FLOWFarm")
return jl_main
Loading