diff --git a/.github/workflows/python-release.yml b/.github/workflows/python-release.yml index fc61205..00ebeb6 100644 --- a/.github/workflows/python-release.yml +++ b/.github/workflows/python-release.yml @@ -1,12 +1,17 @@ name: Create and upload release on: - push: - tags: - - 'v*' + workflow_run: + workflows: ["Python Package using pip"] + types: + - completed jobs: - test-linux: + release: + # Only run if the triggering run was for a tag starting with v + if: > + github.event.workflow_run.conclusion == 'success' && + startsWith(github.event.workflow_run.head_branch, 'v') runs-on: ubuntu-latest strategy: max-parallel: 5 @@ -14,34 +19,12 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Set up Python 3.11.2 - uses: actions/setup-python@v3 + - name: Download pytest-results artifact from previous workflow + uses: actions/download-artifact@v4 with: - python-version: '3.11.2' - - - name: Add conda to system path - run: | - # $CONDA is an environment variable pointing to the root of the miniconda directory - echo $CONDA/bin >> $GITHUB_PATH - - - name: Install dependencies - run: | - conda env update --file environment.yaml - - - name: Lint with flake8 - run: | - conda install flake8 - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - - name: Test with pytest - run: | - source activate bemol - pytest tests/ -vvs - # store the latest run results as release artifact - zip -r pytest-results.zip tests/results/ + name: pytest-results + run-id: ${{ github.event.workflow_run.id }} + path: . - name: Create Release id: create_release diff --git a/.github/workflows/python-test-conda.yml b/.github/workflows/python-test-pip.yml similarity index 50% rename from .github/workflows/python-test-conda.yml rename to .github/workflows/python-test-pip.yml index 3692735..0f851e7 100644 --- a/.github/workflows/python-test-conda.yml +++ b/.github/workflows/python-test-pip.yml @@ -1,6 +1,12 @@ -name: Python Package using Conda +name: Python Package using pip + +on: + push: + branches: + - '*' + tags: + - '*' -on: [push] jobs: test-linux: @@ -16,15 +22,11 @@ jobs: with: python-version: '3.11.2' - - name: Add conda to system path - run: | - # $CONDA is an environment variable pointing to the root of the miniconda directory - echo $CONDA/bin >> $GITHUB_PATH - - name: Install dependencies run: | - conda env update --file environment.yaml - conda install flake8 + python -m pip install --upgrade pip + pip install -r requirements.txt + pip install flake8 - name: Lint with flake8 run: | @@ -33,15 +35,27 @@ jobs: # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - # TODO: conside the latest release, not a hardcoded one! + # considers the latest release, not a hardcoded one! - name: Get reference results run: | - curl -L -o pytest-results.zip "https://github.com/ifpen/bemol/releases/download/v0.0.1/pytest-results.zip" + URL=$(curl -s https://api.github.com/repos/ifpen/bemol/releases/latest \ + | jq -r '.assets[] | select(.name=="pytest-results.zip") | .browser_download_url') + if [ -z "$URL" ] || [ "$URL" = "null" ]; then + echo "pytest-results.zip not found in latest release assets" + exit 1 + fi + curl -L -o pytest-results.zip "$URL" unzip pytest-results.zip mv tests/results tests/ref/ - name: Test with pytest run: | - source activate bemol pytest tests/ -vvs --reference tests/ref/ - \ No newline at end of file + zip -r pytest-results.zip tests/results + + - name: Upload pytest-results artifact + uses: actions/upload-artifact@v4 + with: + name: pytest-results + path: pytest-results.zip + retention-days: 0.020833 # 30 minutes \ No newline at end of file diff --git a/.gitignore b/.gitignore index e9a6557..eef9504 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ **/__pycache__/ -**/.pytest_cache/ \ No newline at end of file +**/.pytest_cache/ + +/.bemol/ \ No newline at end of file diff --git a/README.md b/README.md index d7e7dd4..f869785 100644 --- a/README.md +++ b/README.md @@ -81,14 +81,25 @@ The library is based on [Numpy](https://numpy.org/), [Scipy](https://scipy.org/) [pytest](https://pytest.org/) is as well optional, used for the evaluation of the tests inside the `tests` sub-folder. -The list of considered/tested versions are available in environment.yaml. -For use with conda: +The list of considered/tested versions are available in conda and pip formats. -```bash -conda env create -f environment.yaml -``` +- For use with conda: + + ```bash + conda env create -f environment.yaml + ``` + + Use `conda activate bemol` to use the env. + +- Using pip and venv: + + ```bash + python3 -m venv .bemol + source .bemol/bin/activate + pip install -r requirements.txt + ``` -Use `conda activate bemol` to use the env. + Use `source .bemol/bin/activate` to use the env. ## Authors diff --git a/bemol/bem.py b/bemol/bem.py index 7e79f32..fe62ae3 100644 --- a/bemol/bem.py +++ b/bemol/bem.py @@ -5,60 +5,11 @@ import numpy as np from . import rotor -from . import secondary +from . import correction from . import tools - -class Corrections(object): - """Class for storing corrections. - - Consider all the corrections available in the secondary - models module. If not given by the user considers the base (empty) - correction with the default parameters. - - Parameters - ---------- - corrections : optional - dictionary or list with the secondary corrections, either classes of - instances - in case of custom parameters. If dictionary is - given, the key must be the name of the correction as defined in - secondary.py with a lower first letter. - - """ - def __init__(self,corrections:dict={}): - for name, obj in inspect.getmembers(secondary): - if inspect.isclass(obj): - _name = name[0].lower() + name[1:] - if type(corrections) is dict: - if _name in corrections: - # instantiation of correction with default values - corr = corrections[_name] - corr = corr() if isinstance(corr,type) else corr - setattr(self,_name,corr) - else: - setattr(self,_name,obj.Dummy()) - else: - effect_name = obj.__qualname__ - setattr(self,_name,obj.Dummy()) - # loop for all effects to check if any of the input - # corrections are inner of the available corrections - for corr in corrections: - # instantiation of correction with default values - corr = corr() if isinstance(corr,type) else corr - correction_name = type(corr).__qualname__ - if effect_name in correction_name: - setattr(self,_name,corr) - break - - - def __iter__(self): - """Iterate corrections.""" - for value in self.__dict__.values(): - yield value - - class BaseBEM: """Base BEM class. @@ -89,7 +40,7 @@ def __init__(self,rotor:rotor.Rotor,rho:float=1.225,corrections:dict=None): self.n = len(rotor.sections) if corrections is None: corrections = {} - self.corrections = Corrections(corrections) + self.corrections = correction.Corrections(corrections) diff --git a/bemol/correction.py b/bemol/correction.py new file mode 100644 index 0000000..dba8939 --- /dev/null +++ b/bemol/correction.py @@ -0,0 +1,55 @@ + +import inspect +from types import ModuleType + +from . import secondary + + +class Corrections(object): + """Class for storing corrections. + + Consider all the corrections available in the secondary + models module. If not given by the user considers the base (empty) + correction with the default parameters. + + Parameters + ---------- + corrections : optional + dictionary or list with the secondary corrections, either classes of + instances - in case of custom parameters. If dictionary is + given, the key must be the name of the correction as defined in + secondary.py with a lower first letter. + + """ + def __init__(self,corrections:dict={}): + + for name_mod, mod in inspect.getmembers(secondary): + if not isinstance(mod,ModuleType): continue + effect_name = name_mod[0].lower() + name_mod[1:] + for name_obj, obj in inspect.getmembers(mod): + if inspect.isclass(obj): + if type(corrections) is dict: + if effect_name in corrections: + # instantiation of correction with default values + corr = corrections[effect_name] + corr = corr() if isinstance(corr,type) else corr + setattr(self,effect_name,corr) + else: + setattr(self,effect_name,mod.Dummy()) + else: + setattr(self,effect_name,mod.Dummy()) + # loop for all effects to check if any of the input + # corrections are classes of the available corrections + for corr in corrections: + # instantiation of correction with default values + # TODO: check name before instanciating! + corr = corr() if isinstance(corr,type) else corr + if effect_name in corr.__module__: + setattr(self,effect_name,corr) + break + + + def __iter__(self): + """Iterate corrections.""" + for value in self.__dict__.values(): + yield value diff --git a/bemol/secondary.py b/bemol/secondary.py deleted file mode 100644 index 71bdc64..0000000 --- a/bemol/secondary.py +++ /dev/null @@ -1,202 +0,0 @@ -""" - -Secondary correction models - -""" - -import numpy as np -from . import bem - - -INV_TWO_PI = float(2.0/np.pi) - - -class HubTipLoss: - - class Dummy: - """Empty hub/tip loss correction model.""" - - def __init__(self) -> None: - return - - def __call__(self,*args,**kwargs): - return 1.0 - - class Prandtl: - """Prandt hub/tip loss correction model. - - Parameters - ---------- - epsilon: float, optional - minimal value of F to avoid errors close to/at the bounds of the - blade. Default is 1e-12. - - """ - def __init__(self,epsilon=1e-12) -> None: - self._epsilon = epsilon - return - - def __call__(self,radius,nBlades,hubRadius,tipRadius,inflowAngle): - - fTip = nBlades/2.0*((tipRadius - radius) / (radius*np.abs(np.sin(inflowAngle)))) - fTip = INV_TWO_PI*np.arccos(np.exp(-fTip)) - - fRoot = nBlades/2.0*((radius - hubRadius) / (hubRadius*np.abs(np.sin(inflowAngle)))) - fRoot = INV_TWO_PI*np.arccos(np.exp(-fRoot)) - - return max(fTip*fRoot,self._epsilon) - - -class SkewAngle: - - class Dummy: - """Empty skewed wake model.""" - - def __init__(self) -> None: - return - - def __call__(self,axialInduction,*args,**kwargs): - return axialInduction - - class Burton: - """Burton skewed wake model.""" - - def __init__(self,) -> None: - return - - def __call__(self,axialInduction,yawAngle): - return (0.6 * axialInduction + 1.0) * yawAngle - - -class DynamicInflow: - - class Dummy: - """Empty dynamic inflow model.""" - - def __init__(self) -> None: - return - - def __call__(self,axialInduction,*args,**kwargs): - return axialInduction - - class Knudsen: - """Knudsen dynamic inflow model.""" - - def __init__(self,alphaDynamic=0.3,tauScale=3.0) -> None: - self._alpha0 = alphaDynamic - self.alphaDynamic = alphaDynamic - self.tauScale = tauScale - - def __call__(self,axialInduction,Ux,radius,tStep): - if (tStep == 0.): - raise ValueError( - f'Using a dynamic inflow model with tStep = {tStep} does not make sense!' - ) - # lower bound for wind velocity - kappa = 1. / (self.tauScale * radius / max(1.0,Ux) ) - alphaDynamic = tStep*kappa*(axialInduction - self.alphaDynamic) + self.alphaDynamic - self.alphaDynamic = alphaDynamic - - return alphaDynamic - - def restart(self): - """Restart alpha to the starting value.""" - self.alphaDynamic = self._alpha0 - - -class YawModel: - - class Dummy: - """Empty skewed wake correction model.""" - - def __init__(self) -> None: - return - - def __call__(self,axialInduction,*args,**kwargs): - return axialInduction - - class PittAndPeters: - """Pitt & Peters skewed wake model. - - Pitt & Peters 1980, Theoretical prediction of dynamic-inflow derivatives. - 6th European Rotorcraft & Powered Lift Aircraft Forum - - Snel & Schepers 1995, Joint investigation of dynamic inflow effects and - implementation of an engineering method. Technical Report ECN-C–94-107, - Energy Research Centre of the Netherlands (ECN), - publications.ecn.nl/E/1995/ECN-C--94-107 - - """ - - def __init__(self,factor=15.*np.pi/64.) -> None: - self.factor = factor - - def __call__(self,axialInduction,wakeSkewAngle,azimuthAngle,radius,hubRadius,tipRadius): - return axialInduction*( - 1. + self.factor*np.tan(wakeSkewAngle/2.)*radius/tipRadius*np.sin(azimuthAngle) - ) - - class IFPEN: - """IFPEN skewed wake model. - - Blondel et al. 2017, Improving a BEM yaw model based on NewMexico - experimental data and Vortex/CFD simulations. - CFM 2017 - 23ème Congrès Français de Mécanique - ifp.hal.science/hal-01663643 - - """ - - def __init__(self,A0=0.35,Phi1=-np.pi/9,Phi2=np.pi) -> None: - self.A0 = A0 - self.Phi1 = Phi1 - self.Phi2 = Phi2 - - def __call__(self,axialInduction,wakeSkewAngle,azimuthAngle,radius,hubRadius,tipRadius): - k1 = (1 - self.A0) + self.A0*(radius - hubRadius)/(tipRadius - hubRadius) - k2 = 1 - self.A0*(radius - hubRadius)/(tipRadius - hubRadius) - eta = radius/tipRadius - return axialInduction*( - 1. \ - + k1*eta*np.tan(wakeSkewAngle/2.)*np.sin(azimuthAngle + self.Phi1) \ - + k2*(1 - eta)*np.tan(wakeSkewAngle/2.)*np.sin(azimuthAngle + self.Phi2) - ) - - -class TurbulentWakeState: - - class Dummy: - """Empty high-induction correction model. - - Calls the base thrust coefficient calculation method. - """ - - def __init__(self) -> None: - return - - def __call__(self,axialInduction,lossFactor,skewAngle,*args,**kwargs): - return bem.BaseBEM.CT(axialInduction,lossFactor,skewAngle) - - - class Buhl: - """Buhl's empirical high-induction correction model. - - TODO: need to find the ref to this method. - - """ - - def __init__(self,beta:float=0.4,da:float=0.02) -> None: - self.beta = beta - self.da = da - - def __call__(self,axialInduction,lossFactor,skewAngle): - f0 = bem.BaseBEM.CT(self.beta,lossFactor,skewAngle) - fp0 = ( - bem.BaseBEM.CT(self.beta + self.da,lossFactor,skewAngle) - - bem.BaseBEM.CT(self.beta - self.da,lossFactor,skewAngle) - ) / (2.*self.da) - f1 = 2.*np.cos(skewAngle) - k2 = (f1-f0-fp0*(1.0 - self.beta)) / (1.0 - self.beta)**2. - k1 = fp0 - 2.*k2*self.beta - k0 = f1 - k1 - k2 - - return k0 + k1*axialInduction + k2*axialInduction**2. \ No newline at end of file diff --git a/bemol/secondary/README.md b/bemol/secondary/README.md new file mode 100644 index 0000000..14c06a9 --- /dev/null +++ b/bemol/secondary/README.md @@ -0,0 +1,15 @@ + +# Secondary correction models + +`hubTipLoss.py`: hub/tip loss correction. + +`dynamicInflow.py`: dynamic inflow correction. + +`skewAngle.py`: skew angle correction. + +`turbulentWakeState.py`: turbulent wake state correction. + +`yawModel.py`: yaw models. + +In order to implement a new model, go to the corresponding file and add +a new class similar to the ones already present. \ No newline at end of file diff --git a/bemol/secondary/__init__.py b/bemol/secondary/__init__.py new file mode 100644 index 0000000..e4973cf --- /dev/null +++ b/bemol/secondary/__init__.py @@ -0,0 +1,7 @@ + + +from . import dynamicInflow +from . import hubTipLoss +from . import skewAngle +from . import turbulentWakeState +from . import yawModel diff --git a/bemol/secondary/dynamicInflow.py b/bemol/secondary/dynamicInflow.py new file mode 100644 index 0000000..9917452 --- /dev/null +++ b/bemol/secondary/dynamicInflow.py @@ -0,0 +1,42 @@ + + + +class Dummy: + """Empty dynamic inflow model.""" + + def __init__(self) -> None: + return + + def __call__(self,axialInduction,*args,**kwargs): + return axialInduction + + +class Knudsen: + """Knudsen dynamic inflow model. + + T. Knudsen and T. Bak, "Simple model for describing and estimating wind + turbine dynamic inflow," 2013 American Control Conference, + Washington, DC, USA, 2013, pp. 640-646, doi: 10.1109/ACC.2013.6579909. + + """ + + def __init__(self,alphaDynamic=0.3,tauScale=3.0) -> None: + self._alpha0 = alphaDynamic + self.alphaDynamic = alphaDynamic + self.tauScale = tauScale + + def __call__(self,axialInduction,Ux,radius,tStep): + if (tStep == 0.): + raise ValueError( + f'Using a dynamic inflow model with tStep = {tStep} does not make sense!' + ) + # lower bound for wind velocity + kappa = 1. / (self.tauScale * radius / max(1.0,Ux) ) + alphaDynamic = tStep*kappa*(axialInduction - self.alphaDynamic) + self.alphaDynamic + self.alphaDynamic = alphaDynamic + + return alphaDynamic + + def restart(self): + """Restart alpha to the starting value.""" + self.alphaDynamic = self._alpha0 \ No newline at end of file diff --git a/bemol/secondary/hubTipLoss.py b/bemol/secondary/hubTipLoss.py new file mode 100644 index 0000000..6c3a602 --- /dev/null +++ b/bemol/secondary/hubTipLoss.py @@ -0,0 +1,42 @@ + + +import numpy as np + +INV_TWO_PI = float(2.0/np.pi) + + + +class Dummy: + """Empty hub/tip loss correction model.""" + + def __init__(self) -> None: + return + + def __call__(self,*args,**kwargs): + return 1.0 + + +class Prandtl: + """Prandt hub/tip loss correction model. + + Parameters + ---------- + epsilon: float, optional + minimal value of F to avoid errors close to/at the bounds of the + blade. Default is 1e-12. + + """ + def __init__(self,epsilon=1e-12) -> None: + self._epsilon = epsilon + return + + def __call__(self,radius,nBlades,hubRadius,tipRadius,inflowAngle): + + fTip = nBlades/2.0*((tipRadius - radius) / (radius*np.abs(np.sin(inflowAngle)))) + fTip = INV_TWO_PI*np.arccos(np.exp(-fTip)) + + fRoot = nBlades/2.0*((radius - hubRadius) / (hubRadius*np.abs(np.sin(inflowAngle)))) + fRoot = INV_TWO_PI*np.arccos(np.exp(-fRoot)) + + return max(fTip*fRoot,self._epsilon) + diff --git a/bemol/secondary/skewAngle.py b/bemol/secondary/skewAngle.py new file mode 100644 index 0000000..993ffd1 --- /dev/null +++ b/bemol/secondary/skewAngle.py @@ -0,0 +1,25 @@ + + +class Dummy: + """Empty skewed wake model.""" + + def __init__(self) -> None: + return + + def __call__(self,axialInduction,*args,**kwargs): + return axialInduction + + +class Burton: + """Burton skewed wake model. + + Burton "Handbook of wind energy", 2001, page 105. + + """ + + def __init__(self,) -> None: + return + + def __call__(self,axialInduction,yawAngle): + return (0.6 * axialInduction + 1.0) * yawAngle + diff --git a/bemol/secondary/turbulentWakeState.py b/bemol/secondary/turbulentWakeState.py new file mode 100644 index 0000000..7d59c40 --- /dev/null +++ b/bemol/secondary/turbulentWakeState.py @@ -0,0 +1,46 @@ + + +import numpy as np +from .. import bem + + + + +class Dummy: + """Empty high-induction correction model. + + Calls the base thrust coefficient calculation method. + """ + + def __init__(self) -> None: + return + + def __call__(self,axialInduction,lossFactor,skewAngle,*args,**kwargs): + return bem.BaseBEM.CT(axialInduction,lossFactor,skewAngle) + + +class Buhl: + """Buhl's empirical high-induction correction model. + + Buhl, Jr, M L. "New Empirical Relationship between Thrust Coefficient and + Induction Factor for the Turbulent Windmill State.", Aug. 2005. + https://doi.org/10.2172/15016819 + + """ + + def __init__(self,beta:float=0.4,da:float=0.02) -> None: + self.beta = beta + self.da = da + + def __call__(self,axialInduction,lossFactor,skewAngle): + f0 = bem.BaseBEM.CT(self.beta,lossFactor,skewAngle) + fp0 = ( + bem.BaseBEM.CT(self.beta + self.da,lossFactor,skewAngle) + - bem.BaseBEM.CT(self.beta - self.da,lossFactor,skewAngle) + ) / (2.*self.da) + f1 = 2.*np.cos(skewAngle) + k2 = (f1-f0-fp0*(1.0 - self.beta)) / (1.0 - self.beta)**2. + k1 = fp0 - 2.*k2*self.beta + k0 = f1 - k1 - k2 + + return k0 + k1*axialInduction + k2*axialInduction**2. \ No newline at end of file diff --git a/bemol/secondary/yawModel.py b/bemol/secondary/yawModel.py new file mode 100644 index 0000000..faf80b8 --- /dev/null +++ b/bemol/secondary/yawModel.py @@ -0,0 +1,62 @@ + +import numpy as np + + + + +class Dummy: + """Empty skewed wake correction model.""" + + def __init__(self) -> None: + return + + def __call__(self,axialInduction,*args,**kwargs): + return axialInduction + + +class PittAndPeters: + """Pitt & Peters skewed wake model. + + Pitt & Peters 1980, Theoretical prediction of dynamic-inflow derivatives. + 6th European Rotorcraft & Powered Lift Aircraft Forum + + Snel & Schepers 1995, Joint investigation of dynamic inflow effects and + implementation of an engineering method. Technical Report ECN-C–94-107, + Energy Research Centre of the Netherlands (ECN), + publications.ecn.nl/E/1995/ECN-C--94-107 + + """ + + def __init__(self,factor=15.*np.pi/64.) -> None: + self.factor = factor + + def __call__(self,axialInduction,wakeSkewAngle,azimuthAngle,radius,hubRadius,tipRadius): + return axialInduction*( + 1. + self.factor*np.tan(wakeSkewAngle/2.)*radius/tipRadius*np.sin(azimuthAngle) + ) + + +class IFPEN: + """IFPEN skewed wake model. + + Blondel et al. 2017, Improving a BEM yaw model based on NewMexico + experimental data and Vortex/CFD simulations. + CFM 2017 - 23ème Congrès Français de Mécanique + ifp.hal.science/hal-01663643 + + """ + + def __init__(self,A0=0.35,Phi1=-np.pi/9,Phi2=np.pi) -> None: + self.A0 = A0 + self.Phi1 = Phi1 + self.Phi2 = Phi2 + + def __call__(self,axialInduction,wakeSkewAngle,azimuthAngle,radius,hubRadius,tipRadius): + k1 = (1 - self.A0) + self.A0*(radius - hubRadius)/(tipRadius - hubRadius) + k2 = 1 - self.A0*(radius - hubRadius)/(tipRadius - hubRadius) + eta = radius/tipRadius + return axialInduction*( + 1. \ + + k1*eta*np.tan(wakeSkewAngle/2.)*np.sin(azimuthAngle + self.Phi1) \ + + k2*(1 - eta)*np.tan(wakeSkewAngle/2.)*np.sin(azimuthAngle + self.Phi2) + ) diff --git a/bemol/tools.py b/bemol/tools.py index 3fdad23..6976db8 100644 --- a/bemol/tools.py +++ b/bemol/tools.py @@ -1,7 +1,10 @@ import numpy as np -def calculateVelocity(wind:float,omega:float,rad:float,azi:float,yaw:float,tilt:float,precone:float): +def calculateVelocity( + wind:float,omega:float,rad:float,azi:float, + yaw:float,tilt:float,precone:float + ): """Calculate relative velocity for a given wind configuration Parameters diff --git a/environment.yaml b/environment.yaml index 03cdfdf..a36cd9e 100644 --- a/environment.yaml +++ b/environment.yaml @@ -5,7 +5,7 @@ dependencies: - python==3.11.2 - numpy==1.26.4 - scipy==1.14.1 - - pandas==2.2.1 + - pandas==3.0.2 - pyyaml==6.0.2 - - matplotlib==3.10.1 + - matplotlib==3.10.9 - pytest==8.3.4 \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..860d242 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,31 @@ +colorama==0.4.6 +contourpy==1.3.3 +cycler==0.12.1 +exceptiongroup==1.3.1 +fonttools==4.62.1 +iniconfig==2.3.0 +kiwisolver==1.5.0 +matplotlib==3.10.9 +munkres==1.1.4 +numpy==1.26.4 +packaging==26.2 +pandas==3.0.2 +pillow==12.2.0 +pip==26.0.1 +pluggy==1.6.0 +pyparsing==3.3.2 +PySide6==6.11.0 +pytest==8.3.4 +python-dateutil==2.9.0.post0 +pytz==2026.1.post1 +PyYAML==6.0.2 +scipy==1.14.1 +setuptools==82.0.1 +shiboken6==6.11.0 +six==1.17.0 +tomli==2.4.1 +tornado==6.5.5 +typing_extensions==4.15.0 +tzdata==2026.2 +unicodedata2==17.0.1 +wheel==0.47.0 diff --git a/samples/aligned.py b/samples/aligned.py index 5314b05..72b4163 100644 --- a/samples/aligned.py +++ b/samples/aligned.py @@ -26,11 +26,11 @@ rho = 1.191 corrections = ( - bemol.secondary.HubTipLoss.Prandtl, - bemol.secondary.SkewAngle.Burton, - bemol.secondary.YawModel.PittAndPeters, - bemol.secondary.DynamicInflow.Dummy, - bemol.secondary.TurbulentWakeState.Buhl, + bemol.secondary.hubTipLoss.Prandtl, + bemol.secondary.skewAngle.Burton, + bemol.secondary.yawModel.PittAndPeters, + bemol.secondary.dynamicInflow.Dummy, + bemol.secondary.turbulentWakeState.Buhl, ) # all angles are null, flow completly aligned diff --git a/samples/iea15mw.py b/samples/iea15mw.py index 2667b6e..d01f55e 100644 --- a/samples/iea15mw.py +++ b/samples/iea15mw.py @@ -28,8 +28,8 @@ rho = 1.225 corrections = ( - bemol.secondary.HubTipLoss.Prandtl, - bemol.secondary.TurbulentWakeState.Buhl, + bemol.secondary.hubTipLoss.Prandtl, + bemol.secondary.turbulentWakeState.Buhl, ) # all angles are null, flow completly aligned diff --git a/samples/pitch_maneuver.py b/samples/pitch_maneuver.py index 3a3acca..21c25b7 100644 --- a/samples/pitch_maneuver.py +++ b/samples/pitch_maneuver.py @@ -39,11 +39,11 @@ mexico = bemol.rotor.mexico corrections = [ - bemol.secondary.HubTipLoss.Prandtl, - bemol.secondary.SkewAngle.Burton, - bemol.secondary.YawModel.Dummy, - bemol.secondary.DynamicInflow.Knudsen, - bemol.secondary.TurbulentWakeState.Buhl, + bemol.secondary.hubTipLoss.Prandtl, + bemol.secondary.skewAngle.Burton, + bemol.secondary.yawModel.Dummy, + bemol.secondary.dynamicInflow.Knudsen, + bemol.secondary.turbulentWakeState.Buhl, ] times = np.arange(0.0,50.0,tStep) diff --git a/samples/yaw.py b/samples/yaw.py index f29d70c..cfbe58a 100644 --- a/samples/yaw.py +++ b/samples/yaw.py @@ -40,9 +40,9 @@ mexico = bemol.rotor.mexico corrections = ( - bemol.secondary.HubTipLoss.Prandtl, - bemol.secondary.SkewAngle.Burton, - bemol.secondary.TurbulentWakeState.Buhl, + bemol.secondary.hubTipLoss.Prandtl, + bemol.secondary.skewAngle.Burton, + bemol.secondary.turbulentWakeState.Buhl, ) diff --git a/samples/yaw_models.py b/samples/yaw_models.py index 556b32b..fbdb10b 100644 --- a/samples/yaw_models.py +++ b/samples/yaw_models.py @@ -39,9 +39,9 @@ mexico = bemol.rotor.mexico base_corrections = [ - bemol.secondary.HubTipLoss.Prandtl, - bemol.secondary.SkewAngle.Burton, - bemol.secondary.TurbulentWakeState.Buhl, + bemol.secondary.hubTipLoss.Prandtl, + bemol.secondary.skewAngle.Burton, + bemol.secondary.turbulentWakeState.Buhl, ] @@ -49,7 +49,7 @@ for yaw_model in ('Dummy','PittAndPeters','IFPEN'): corrections = base_corrections.copy() - corrections.append(getattr(bemol.secondary.YawModel,yaw_model)) + corrections.append(getattr(bemol.secondary.yawModel,yaw_model)) solver = bemol.ning.NingUncoupled(mexico,rho,corrections) forces, _, azimuths = solver.cycle( diff --git a/test.py b/test.py new file mode 100644 index 0000000..08077ab --- /dev/null +++ b/test.py @@ -0,0 +1,12 @@ + +import bemol + +"""Test definition of secondary effects.""" +dummy_rotor = bemol.rotor.mexico +# model = bemol.bem.BaseBEM(dummy_rotor,corrections={}) +model = bemol.bem.BaseBEM( + dummy_rotor, + corrections=[ + bemol.secondary.hubTipLoss.Prandtl, + bemol.secondary.skewAngle.Burton + ]) \ No newline at end of file diff --git a/tests/test_result.py b/tests/test_result.py index 377ccb2..4536630 100644 --- a/tests/test_result.py +++ b/tests/test_result.py @@ -32,7 +32,8 @@ def change_test_dir(request,monkeypatch): @pytest.fixture(scope='module',autouse=True,) def reference_folder(request): # define the reference folder based on current folder - reference_path = request.config.reference + try: reference_path = request.config.reference + except AttributeError: return None # doing nothing if reference not given return Path( reference_path ).resolve() @@ -112,10 +113,13 @@ def test_compare(folder,run,request,reference_folder): new_data = pd.read_csv(new_filename) # only comparing if reference folder available - if reference_folder.exists(): + if reference_folder is not None and reference_folder.exists(): ref_filename = reference_folder / name_run / filename ref_data = pd.read_csv(ref_filename) - pd.testing.assert_frame_equal(ref_data,new_data) + pd.testing.assert_frame_equal( + ref_data,new_data, + check_exact=False,rtol=0.0001, + ) else: # for the moment just doing nothing if reference folder not # available! Make it at least a warning in the future. diff --git a/tests/test_unit.py b/tests/test_unit.py index cc7820a..1824bab 100644 --- a/tests/test_unit.py +++ b/tests/test_unit.py @@ -18,6 +18,7 @@ def test_airfoil(): assert airfoil._cl.size > 1 + def test_rotor(): """Test definition of the rotor class.""" @@ -36,7 +37,7 @@ def test_rotor(): -def test_secondary_module(): +def test_secondary_module_default(): """Test definition of secondary effects.""" dummy_rotor = bemol.rotor.mexico @@ -49,49 +50,74 @@ def test_secondary_module(): assert hasattr(model.corrections,'yawModel') assert hasattr(model.corrections,'turbulentWakeState') - # check if the Dummy is selected everytime + + +def test_secondary_module_dummy(): + """Test if the Dummy is selected everytime""" + dummy_rotor = bemol.rotor.mexico + model = bemol.bem.BaseBEM(dummy_rotor) - assert model.corrections.hubTipLoss.__class__ is bemol.secondary.HubTipLoss.Dummy - assert model.corrections.skewAngle.__class__ is bemol.secondary.SkewAngle.Dummy - assert model.corrections.dynamicInflow.__class__ is bemol.secondary.DynamicInflow.Dummy - assert model.corrections.yawModel.__class__ is bemol.secondary.YawModel.Dummy - assert model.corrections.turbulentWakeState.__class__ is bemol.secondary.TurbulentWakeState.Dummy + assert model.corrections.hubTipLoss.__class__ is bemol.secondary.hubTipLoss.Dummy + assert model.corrections.skewAngle.__class__ is bemol.secondary.skewAngle.Dummy + assert model.corrections.dynamicInflow.__class__ is bemol.secondary.dynamicInflow.Dummy + assert model.corrections.yawModel.__class__ is bemol.secondary.yawModel.Dummy + assert model.corrections.turbulentWakeState.__class__ is bemol.secondary.turbulentWakeState.Dummy + + + +def test_secondary_module_custom_list(): + """Test define a model with a few custom corrections.""" + dummy_rotor = bemol.rotor.mexico - # define a model with a few custom corrections model = bemol.bem.BaseBEM( dummy_rotor, - corrections=[bemol.secondary.HubTipLoss.Prandtl,bemol.secondary.SkewAngle.Burton] + corrections=[bemol.secondary.hubTipLoss.Prandtl,bemol.secondary.skewAngle.Burton] ) - assert model.corrections.hubTipLoss.__class__ is bemol.secondary.HubTipLoss.Prandtl - assert model.corrections.skewAngle.__class__ is bemol.secondary.SkewAngle.Burton - assert model.corrections.dynamicInflow.__class__ is bemol.secondary.DynamicInflow.Dummy - assert model.corrections.yawModel.__class__ is bemol.secondary.YawModel.Dummy - assert model.corrections.turbulentWakeState.__class__ is bemol.secondary.TurbulentWakeState.Dummy + assert model.corrections.hubTipLoss.__class__ is bemol.secondary.hubTipLoss.Prandtl + assert model.corrections.skewAngle.__class__ is bemol.secondary.skewAngle.Burton + assert model.corrections.dynamicInflow.__class__ is bemol.secondary.dynamicInflow.Dummy + assert model.corrections.yawModel.__class__ is bemol.secondary.yawModel.Dummy + assert model.corrections.turbulentWakeState.__class__ is bemol.secondary.turbulentWakeState.Dummy + + + +def test_secondary_module_custom_dict(): + """Same as before, but with dict input define a model with a few custom corrections""" + dummy_rotor = bemol.rotor.mexico - # same as before, but with dict input define a model with a few custom corrections model = bemol.bem.BaseBEM( dummy_rotor, corrections=dict( - hubTipLoss=bemol.secondary.HubTipLoss.Prandtl, - skewAngle=bemol.secondary.SkewAngle.Burton + hubTipLoss=bemol.secondary.hubTipLoss.Prandtl, + skewAngle=bemol.secondary.skewAngle.Burton ) ) - assert model.corrections.hubTipLoss.__class__ is bemol.secondary.HubTipLoss.Prandtl - assert model.corrections.skewAngle.__class__ is bemol.secondary.SkewAngle.Burton - assert model.corrections.dynamicInflow.__class__ is bemol.secondary.DynamicInflow.Dummy - assert model.corrections.yawModel.__class__ is bemol.secondary.YawModel.Dummy - assert model.corrections.turbulentWakeState.__class__ is bemol.secondary.TurbulentWakeState.Dummy - - # mix class and instance - hub_corr = bemol.secondary.HubTipLoss.Prandtl() + assert model.corrections.hubTipLoss.__class__ is bemol.secondary.hubTipLoss.Prandtl + assert model.corrections.skewAngle.__class__ is bemol.secondary.skewAngle.Burton + assert model.corrections.dynamicInflow.__class__ is bemol.secondary.dynamicInflow.Dummy + assert model.corrections.yawModel.__class__ is bemol.secondary.yawModel.Dummy + assert model.corrections.turbulentWakeState.__class__ is bemol.secondary.turbulentWakeState.Dummy + + + +def test_secondary_module_custom_mixed(): + """Test mix class and instance for correction input.""" + dummy_rotor = bemol.rotor.mexico + + hub_corr = bemol.secondary.hubTipLoss.Prandtl() model = bemol.bem.BaseBEM( - dummy_rotor,corrections=[hub_corr,bemol.secondary.SkewAngle.Burton] + dummy_rotor,corrections=[hub_corr,bemol.secondary.skewAngle.Burton] ) assert model.corrections.hubTipLoss is hub_corr - assert model.corrections.skewAngle.__class__ is bemol.secondary.SkewAngle.Burton + assert model.corrections.skewAngle.__class__ is bemol.secondary.skewAngle.Burton + - # check if corrections of nested solvers are the same instance +def test_secondary_module_nested_solvers(): + """Check if corrections of nested solvers are the same instance.""" ## TOOD: maybe this is not always the wanted behavior! + dummy_rotor = bemol.rotor.mexico + corrections = {} + solver_uncoupled = bemol.ning.NingUncoupled(dummy_rotor,1.0,corrections) solver_coupled = bemol.ning.NingCoupled(dummy_rotor,1.0,corrections) assert solver_coupled.corrections.dynamicInflow is not solver_uncoupled.corrections.dynamicInflow