Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
10e36db
Outline of floris integration
skygering Dec 21, 2025
4253147
First attempt with saving bem solutions
skygering Dec 22, 2025
ad926ef
Working attempt, but power values uncertian
skygering Jan 5, 2026
a6f936d
floris interface tests finished
skygering Jan 7, 2026
6dd4da0
Add files to poetry
skygering Jan 7, 2026
71c808a
Save needed CSV files on GitHub
skygering Jan 7, 2026
b7a69b1
Remove | that isn't python 3.9 compatible
skygering Jan 7, 2026
f696c68
Remove non-python 3.9 compatible file call
skygering Jan 7, 2026
1a8b424
Add larger example and clean up comments
skygering Jan 8, 2026
92d3cda
Add docstring
skygering Jan 8, 2026
d4b2d1d
Add CT CSV file for IEA15MW
skygering Jan 8, 2026
6688d21
Use more condensed validation data and improve validation figures
skygering Jan 20, 2026
a5798d4
Switch default to UMM Rotor-averaged and add more rotors to example f…
skygering Jan 21, 2026
daa0d95
Remove interpolation of setpoints from timing
skygering Feb 28, 2026
204c208
Merge branch 'master' into sg/floris_interface
skygering Mar 1, 2026
a585640
Updated example 6 for non-vectorized code
skygering Mar 1, 2026
f6f0368
Add in example timing script
skygering Mar 1, 2026
b2c1094
Add in plotting code and fix errors in timing code
skygering Mar 1, 2026
3659c29
Update timing plots
skygering Mar 1, 2026
57ff90f
MIT Rotor Vectorization (#20)
skygering Apr 10, 2026
467cf2c
Add in ROSCO interface
skygering Jun 1, 2026
21cf7a2
Working ROSCO controls in FLORIS
skygering Jun 2, 2026
178acb7
Yaw and deg vs rad bugs fixed
skygering Jun 2, 2026
03c0b5c
Attempt at debugging yaw difference
skygering Jun 4, 2026
5be3048
Add example CSVs
skygering Jun 4, 2026
7b96114
Working version of stead-state ROSCO simulation code
skygering Jun 26, 2026
ee1e2d5
Working 2D interpolator
skygering Jul 3, 2026
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
14 changes: 13 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ fig*
*.tar
*.whl

# Allow these CSVs needed for the floris interface
!MITRotor/FlorisInterface/IEA_15mw_rotor.csv
!examples/examples_in/*

Validation/

### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down Expand Up @@ -188,4 +194,10 @@ poetry.toml
# LSP config files
pyrightconfig.json

# End of https://www.toptal.com/developers/gitignore/api/python
# End of https://www.toptal.com/developers/gitignore/api/python

.DS_Store
examples/Tune_Cases/
*dbg*
DISCON.IN
*txt
11 changes: 7 additions & 4 deletions MITRotor/Aerodynamics.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from numpy.typing import ArrayLike

from .RotorDefinition import RotorDefinition
from .Geometry import BEMGeometry
from .Geometry import BEMGeometry, expand_to_Nr_Ntheta
from UnifiedMomentumModel.Utilities.Geometry import calc_eff_yaw

__all__ = [
Expand Down Expand Up @@ -269,13 +269,16 @@ def __call__(
AerodynamicProperties: Calculated aerodynamic properties stored in AerodynamicProperties object.

"""
tsr = expand_to_Nr_Ntheta(tsr)
pitch = expand_to_Nr_Ntheta(pitch)
# calculate values in "yaw-only" frame
local_yaw = -self.eff_yaw
Vax = U * ((1 - an) * np.cos(local_yaw))
theta_eff = geom.theta_mesh + self.delta_theta
Vtan = (
(1 + aprime) * tsr * geom.mu_mesh
- U * (1 - an)
* np.cos(self.eff_theta_mesh)
* np.cos(theta_eff)
* np.sin(local_yaw)
)

Expand All @@ -291,8 +294,8 @@ def __call__(
an = an,
aprime = aprime,
solidity = solidity,
U = U * np.ones(geom.shape),
wdir = wdir * np.ones(geom.shape),
U = U * np.ones_like(geom.mu_mesh),
wdir = wdir * np.ones_like(geom.mu_mesh),
Vax = Vax,
Vtan = Vtan,
aoa = aoa,
Expand Down
95 changes: 65 additions & 30 deletions MITRotor/BEMSolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@

from . import Momentum, TipLoss
from .Aerodynamics import AerodynamicModel, AerodynamicProperties, DefaultAerodynamics
from .Geometry import BEMGeometry
from .Geometry import BEMGeometry, expand_to_Np, expand_to_Nr_Ntheta
from .RotorDefinition import RotorDefinition
from .TangentialInduction import DefaultTangentialInduction, TangentialInductionModel
from UnifiedMomentumModel.Utilities.Geometry import calc_eff_yaw



def average(geometry: BEMGeometry, value: ArrayLike, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
# Assuming the function returns a 2D grid of values

Expand Down Expand Up @@ -58,13 +59,13 @@ def solidity(self, grid: Literal["sector ", "annulus", "rotor"] = "rotor"):
def U(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
return average(self.geom, self.aero_props.U, grid)

def wdir(self, grid: Literal["sector ", "annulus", "rotor"] = "rotor"):
def wdir(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
return average(self.geom, self.aero_props.wdir, grid)

def Vax(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
return average(self.geom, self.aero_props.Vax, grid)

def Vtan(self, grid: Literal["sector ", "annulus", "rotor"] = "rotor"):
def Vtan(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
return average(self.geom, self.aero_props.Vtan, grid)

def W(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
Expand All @@ -91,29 +92,27 @@ def Ctan(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
def Cx(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
return average(self.geom, self.aero_props.C_x_corr, grid)

def Ctau(self, grid: Literal["sector ", "annulus", "rotor"] = "rotor"):
def Ctau(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
return average(self.geom, self.aero_props.C_tau_corr, grid)

def Ctau_uncorr(self, grid: Literal["sector ", "annulus", "rotor"] = "rotor"):
def Ctau_uncorr(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
return average(self.geom, self.aero_props.C_tau, grid)

def F(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
return average(self.geom, self.aero_props.F, grid)

def Cp(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
dCp = (
self.tsr
* self.geom.mu_mesh
* self.Ctau_uncorr(grid="sector")
)
tsr = np.asarray(self.tsr)
if tsr.ndim == 1:
tsr = expand_to_Nr_Ntheta(tsr)
dCp = (tsr * self.geom.mu_mesh * self.Ctau_uncorr(grid="sector"))
return average(self.geom, dCp, grid=grid)

def Cp_corr(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
dCp = (
self.tsr
* self.geom.mu_mesh
* self.Ctau(grid="sector")
)
tsr = np.asarray(self.tsr)
if tsr.ndim == 1:
tsr = expand_to_Nr_Ntheta(tsr)
dCp = (tsr * self.geom.mu_mesh * self.Ctau(grid="sector"))
return average(self.geom, dCp, grid=grid)

def Ct(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
Expand All @@ -128,6 +127,13 @@ def Ctprime(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
eff_yaw = calc_eff_yaw(self.yaw, self.tilt)
Ctprime = self.Ct(grid="sector") / ((1 - self.a(grid="sector")) ** 2 * np.cos(eff_yaw) ** 2)
return average(self.geom, Ctprime, grid=grid)

# TODO: make sure that this is correct!! Check with np.isclose(Cp, tsr * Cq)
def Cq(self, grid: Literal["sector", "annulus", "rotor"] = "rotor"):
mu = np.asarray(self.geom.mu_mesh)
Ctau = self.Ctau_uncorr(grid="sector")
dCq = mu * Ctau
return average(self.geom, dCq, grid=grid)


@adaptivefixedpointiteration(max_iter=500, relaxations=[0.25, 0.5, 0.96])
Expand Down Expand Up @@ -169,21 +175,37 @@ def sample_points(self, yaw: float = 0.0, tilt: float = 0.0) -> tuple[ArrayLike,
return X, Y, Z

def pre_process(self, pitch, tsr, yaw = 0, tilt = 0, **kwargs):
pitch, tsr = np.asarray(pitch), np.asarray(tsr)
yaw, tilt = np.asarray(yaw), np.asarray(tilt)
self.scalar_inputs = (pitch.ndim == 0) & (tsr.ndim == 0) & (yaw.ndim == 0) & (tilt.ndim == 0)
if not self.scalar_inputs:
assert len(pitch) == len(tsr) == len(yaw) == len(tilt), "Setpoint arrays should be the same lenght"
# switch reference frame to a "yaw-only" frame where y' is aligned with the lateral wake
self.aerodynamic_model.eff_yaw = calc_eff_yaw(yaw, tilt)
if tilt == 0:
dtheta = 0
elif yaw == 0:
dtheta = np.pi / 2
else: # non-zero yaw and tilt
sin_eff = np.sin(self.aerodynamic_model.eff_yaw)
dtheta = np.arccos(np.sin(yaw) / sin_eff)
self.aerodynamic_model.eff_theta_mesh = self.geometry.theta_mesh + dtheta
yaw, tilt = np.broadcast_arrays(yaw, tilt)
eff_yaw = calc_eff_yaw(yaw, tilt)
# initialize dtheta
dtheta = np.zeros_like(eff_yaw, dtype=float)
# masks
yaw_zero = (yaw == 0)
not_tilt_zero = np.logical_not(tilt == 0)
# case1: yaw == 0 and tilt != 0
case1 = yaw_zero & not_tilt_zero
dtheta[case1] = np.pi / 2
# case2: yaw != 0 and tilt != 0
case2 = np.logical_not(yaw_zero) & not_tilt_zero
dtheta[case2] = np.arccos(
np.sin(yaw[case2]) / np.sin(eff_yaw[case2])
)
# expand everything to the correct dimensions to ensure broadcasting
self.aerodynamic_model.eff_yaw = expand_to_Nr_Ntheta(eff_yaw)
self.aerodynamic_model.delta_theta = expand_to_Nr_Ntheta(dtheta)
self.geometry.mu_mesh = expand_to_Np(self.geometry.mu_mesh)
self.geometry.theta_mesh = expand_to_Np(self.geometry.theta_mesh)
return

def initial_guess(self, *args, **kwargs) -> Tuple[ArrayLike, ...]:
a = (1 / 3) * np.ones(self.geometry.shape)
aprime = np.zeros(self.geometry.shape)
a = (1 / 3) * np.ones_like(self.geometry.mu_mesh)
aprime = np.zeros_like(self.geometry.mu_mesh)

return a, aprime

Expand All @@ -198,8 +220,8 @@ def residual(
tilt: ArrayLike = 0.0,
) -> Tuple[ArrayLike, ...]:
an, aprime = x
U = np.ones(self.geometry.shape) if U is None else U
wdir = np.zeros(self.geometry.shape) if wdir is None else wdir
U = np.ones_like(self.geometry.mu_mesh) if U is None else U
wdir = np.zeros_like(self.geometry.mu_mesh) if wdir is None else wdir

aero_props = self.aerodynamic_model(
an = an,
Expand All @@ -221,12 +243,25 @@ def residual(
return e_an, e_aprime

def post_process(self, result: FixedPointIterationResult, pitch, tsr, yaw = 0, U=None, wdir=None, tilt = 0.0) -> BEMSolution:
U = np.ones(self.geometry.shape) if U is None else U
wdir = np.zeros(self.geometry.shape) if wdir is None else wdir
U = np.ones_like(self.geometry.mu_mesh) if U is None else U
wdir = np.zeros_like(self.geometry.mu_mesh) if wdir is None else wdir
an, aprime = result.x
aero_props = self.aerodynamic_model(an, aprime, pitch, tsr, yaw, self.rotor, self.geometry, U, wdir, tilt = tilt)
aero_props.F = self.tiploss_model(aero_props, pitch, tsr, yaw, self.rotor, self.geometry, tilt = tilt)
avg_Ct = average(self.geometry, aero_props.C_x)
u4,v4,w4 = self.momentum_model.compute_initial_wake_velocities(avg_Ct, yaw, tilt = tilt)

if self.scalar_inputs: # if all setpoints were scalars
# return single values as scalars
pitch, tsr, yaw, tilt = [np.asarray(x).item() for x in (pitch, tsr, yaw, tilt)]
u4, v4, w4 = [np.asarray(x).item() for x in (u4, v4, w4)]
# remove unneeded extra axis from Ntheta x Nr arrays
an = np.squeeze(an)
aprime = np.squeeze(aprime)
self.geometry.theta_mesh = np.squeeze(self.geometry.theta_mesh)
self.geometry.mu_mesh = np.squeeze(self.geometry.mu_mesh)
for key, value in vars(aero_props).items():
if isinstance(value, np.ndarray):
setattr(aero_props, key, np.squeeze(value))

return BEMSolution(pitch, tsr, yaw, aero_props, self.geometry, result.converged, result.niter, u4, v4, tilt = tilt, w4 = w4)
Loading
Loading