Skip to content

Commit da1c293

Browse files
committed
Added FLOWFarm integration tests. Added notes about flowfarm multithreading.
1 parent 0279c24 commit da1c293

File tree

18 files changed

+1378
-208
lines changed

18 files changed

+1378
-208
lines changed

.github/workflows/julia-tests.yaml

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
name: FLOWFarm integration tests (Julia)
2+
3+
on:
4+
push:
5+
paths:
6+
- "ard/flowfarm/**"
7+
- "ard/farm_aero/flowfarm.py"
8+
- "test/flowfarm/**"
9+
pull_request:
10+
paths:
11+
- "ard/flowfarm/**"
12+
- "ard/farm_aero/flowfarm.py"
13+
- "test/flowfarm/**"
14+
workflow_dispatch: # allow manual runs from the Actions UI
15+
16+
jobs:
17+
18+
test-julia:
19+
name: Run FLOWFarm integration tests
20+
runs-on: ubuntu-latest
21+
22+
steps:
23+
- name: Checkout code
24+
uses: actions/checkout@v4
25+
26+
- name: Set up Python 3.12
27+
uses: actions/setup-python@v5
28+
with:
29+
python-version: "3.12"
30+
31+
- name: Set up Julia
32+
uses: julia-actions/setup-julia@v2
33+
with:
34+
version: "1" # latest stable 1.x
35+
36+
- name: Cache Julia packages
37+
uses: julia-actions/cache@v2
38+
39+
- name: Install Ard with FLOWFarm extras
40+
run: pip install ".[dev,flowfarm]"
41+
42+
- name: Pre-instantiate Julia environment
43+
run: |
44+
julia --project=ard/flowfarm/julia_env -e "using Pkg; Pkg.resolve(); Pkg.instantiate()"
45+
46+
- name: Run FLOWFarm integration tests
47+
run: |
48+
pytest -m julia test/flowfarm/integration \
49+
--cov=ard \
50+
--cov-report=term-missing \
51+
-v

.github/workflows/python-tests-consolidated.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ jobs:
8484
pip install .[dev]
8585
- name: Run unit tests with coverage
8686
run: |
87-
pytest --cov=ard --cov-fail-under=80 test/ard/unit
87+
pytest --cov=ard --cov-fail-under=80 \
88+
test/ard/unit \
89+
test/flowfarm/unit \
90+
-m "not julia"
8891
8992
test-system:
9093
name: Run system tests

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
11

2-
### ARD DEVELOPMENT IGNORES
2+
### ARD DEVELOPMENT IGNORES
33

44
.vscode
55
case_files
66
ard_prob_out
77

8+
### JULIA
9+
# Manifest.toml is user-generated (depends on local Julia version).
10+
# Project.toml is committed; Manifest.toml is rebuilt automatically on first use.
11+
ard/flowfarm/julia_env/Manifest.toml
12+
813
### MACOS DEFAULT IGNORES
914

1015
.DS_Store

ard/farm_aero/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
from . import floris
2+
from . import flowfarm
23
from . import placeholder
34
from . import templates

ard/farm_aero/flowfarm.py

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,14 @@
11
import os
2-
import sys
3-
from pathlib import Path
42

53
import numpy as np
64
import pandas as pd
75

86
from ard.farm_aero.floris import create_FLORIS_turbine_from_windIO
9-
try:
10-
from flowfarm.flowfarm_model import (
11-
ensure_flowfarm_loaded,
12-
resolve_turbine_inputs_for_flowfarm,
13-
resolve_wake_model_inputs_for_flowfarm,
14-
)
15-
except ModuleNotFoundError:
16-
# Local checkout fallback: add repository-level Ard/ to sys.path.
17-
repo_ard_dir = Path(__file__).resolve().parents[2]
18-
if str(repo_ard_dir) not in sys.path:
19-
sys.path.insert(0, str(repo_ard_dir))
20-
from flowfarm.flowfarm_model import (
21-
ensure_flowfarm_loaded,
22-
resolve_turbine_inputs_for_flowfarm,
23-
resolve_wake_model_inputs_for_flowfarm,
24-
)
7+
from ard.flowfarm.flowfarm_model import (
8+
ensure_flowfarm_loaded,
9+
resolve_turbine_inputs_for_flowfarm,
10+
resolve_wake_model_inputs_for_flowfarm,
11+
)
2512

2613
import ard.farm_aero.templates as templates
2714

ard/flowfarm/README.md

Lines changed: 113 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,88 @@
22

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

5+
## Julia setup (required before first use)
6+
7+
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.
8+
9+
### 1. Install Julia via juliaup (recommended)
10+
11+
[juliaup](https://github.com/JuliaLang/juliaup) is the official Julia version manager. Install it with:
12+
13+
```bash
14+
curl -fsSL https://install.julialang.org | sh
15+
```
16+
17+
Install any recent stable Julia release (1.10 or later):
18+
19+
```bash
20+
juliaup add release
21+
juliaup default release
22+
```
23+
24+
Verify:
25+
26+
```bash
27+
julia --version
28+
```
29+
30+
Ard's Julia environment has no hard version pin. The `Manifest.toml` is not committed to the repository — it is generated locally the first time you run a FLOWFarm component, so it will always match your installed Julia version. If you need to generate it ahead of time (e.g. on a cluster before a job runs), see step 2.
31+
32+
### 2. Pre-generate the Julia environment (optional)
33+
34+
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:
35+
36+
```bash
37+
julia --project="<path-to-ard>/ard/flowfarm/julia_env" -e "using Pkg; Pkg.resolve(); Pkg.instantiate()"
38+
```
39+
40+
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.
41+
42+
### 3. Install the JuliaCall Python package
43+
44+
```bash
45+
pip install juliacall
46+
```
47+
48+
`juliacall` is not listed in Ard's core dependencies because it is only needed for FLOWFarm. Install it separately before using FLOWFarm components.
49+
550
## What this integration does
651

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

12-
## Threading behavior
57+
## Threading and parallelism
58+
59+
### OpenMDAO does not multithread
1360

14-
- Ard supports Julia threading through JuliaCall.
15-
- The FLOWFarm update callback used by Ard is implemented in pure Julia (not Python callback) to avoid PythonCall thread-safety crashes.
16-
- If you configure Julia threads with environment variables, set them **before** importing Ard/JuliaCall.
61+
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.
1762

18-
Recommended JuliaCall env options for threaded runs:
63+
### Julia internal threading (FLOWFarm parallelism)
1964

20-
- `PYTHON_JULIACALL_THREADS=<N or auto>`
21-
- `PYTHON_JULIACALL_HANDLE_SIGNALS=yes`
22-
- (optional) `OPENBLAS_NUM_THREADS=1`, `OMP_NUM_THREADS=1` to avoid nested thread oversubscription
65+
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.
66+
67+
Julia's thread count is fixed at startup and cannot be changed at runtime. Configure it **before** importing Ard or JuliaCall:
68+
69+
```python
70+
import os
71+
os.environ["PYTHON_JULIACALL_THREADS"] = "4" # or "auto" to use all cores
72+
os.environ["PYTHON_JULIACALL_HANDLE_SIGNALS"] = "yes"
73+
```
74+
75+
For threaded runs on shared-memory machines, also consider limiting BLAS and OpenMP thread pools to avoid nested oversubscription (Julia threads × BLAS threads × cores):
76+
77+
```python
78+
os.environ["OPENBLAS_NUM_THREADS"] = "1"
79+
os.environ["OMP_NUM_THREADS"] = "1"
80+
```
81+
82+
These must be set before the first `import ard` call in your script or notebook.
83+
84+
### Why a pure Julia callback
85+
86+
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.
2387

2488
## Tolerance behavior
2589

@@ -42,11 +106,45 @@ modeling_options:
42106

43107
## Troubleshooting
44108

45-
- Kernel/process crash when threads > 1:
46-
- Ensure pure Julia callback path is active (current Ard default).
47-
- Ensure thread env vars are set before importing Ard.
48-
- Start with `PYTHON_JULIACALL_THREADS=1`, then increase.
49-
- Julia environment mismatch errors:
50-
- Re-instantiate the local Julia env in `julia_env`.
51-
- Confirm FLOWFarm revision/pin is compatible with your Julia runtime.
109+
### Julia manifest warnings on first run
110+
111+
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:
112+
113+
```bash
114+
julia --project="<path-to-ard>/ard/flowfarm/julia_env" -e "using Pkg; Pkg.resolve(); Pkg.instantiate()"
115+
```
116+
117+
Then restart your Jupyter kernel. The `Manifest.toml` is not committed to the repository — it is always generated locally for your Julia version.
118+
119+
### Revise / DistributedExt error in Jupyter
120+
121+
```
122+
Error during loading of extension DistributedExt of Revise
123+
```
124+
125+
This comes from your **global** Julia environment, not Ard's. JuliaCall triggers the IPython/Jupyter juliacall extension on import, which loads Revise from your global env. Fix it by running:
126+
127+
```bash
128+
julia -e "using Pkg; Pkg.add(\"Distributed\"); Pkg.resolve()"
129+
```
130+
131+
### Wrong Julia version being used
132+
133+
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.
134+
135+
To force a specific version for a notebook session, add this to the **first cell** before any other imports:
136+
137+
```python
138+
import os
139+
os.environ["PYTHON_JULIACALL_EXE"] = "julia +1.10"
140+
os.environ["ARD_FLOWFARM_RESPECT_JULIACALL_ENV"] = "1"
141+
```
142+
143+
`ARD_FLOWFARM_RESPECT_JULIACALL_ENV=1` is required — without it Ard's bootstrap strips the override.
144+
145+
### Kernel/process crash when threads > 1
146+
147+
- Ensure pure Julia callback path is active (current Ard default).
148+
- Ensure thread env vars are set before importing Ard.
149+
- Start with `PYTHON_JULIACALL_THREADS=1`, then increase.
52150

0 commit comments

Comments
 (0)