diff --git a/.github/workflows/CI_WISDEM.yml b/.github/workflows/CI_WISDEM.yml index a3fd18083..cb906300c 100644 --- a/.github/workflows/CI_WISDEM.yml +++ b/.github/workflows/CI_WISDEM.yml @@ -17,7 +17,7 @@ jobs: fail-fast: False matrix: os: [ubuntu-latest, macos-14, windows-latest] - python-version: ["3.11", "3.12", "3.13"] + python-version: ["3.12", "3.13", "3.14"] steps: - name: checkout repository @@ -46,7 +46,7 @@ jobs: - name: Add dependencies windows specific if: contains( matrix.os, 'windows') run: | - conda install -y m2w64-toolchain libpython + conda install -y gfortran gcc - name: Debug config run: | @@ -75,7 +75,7 @@ jobs: # Run coveralls - name: Run coveralls - if: contains( matrix.os, 'ubuntu') && contains( matrix.python-version, '3.12') + if: contains( matrix.os, 'ubuntu') && contains( matrix.python-version, '3.13') #uses: coverallsapp/github-action@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} @@ -94,7 +94,7 @@ jobs: fail-fast: False matrix: os: [ubuntu-latest, windows-latest, macos-14] #mac-13 intel, mac-latest is arm - python-version: ["3.12"] + python-version: ["3.13"] steps: - name: Setup GNU Fortran diff --git a/environment.yml b/environment.yml index c3a83366a..3de791f1e 100644 --- a/environment.yml +++ b/environment.yml @@ -4,8 +4,8 @@ channels: dependencies: # NOTE: uncomment the OS-dependent lines below as needed - # - m2w64-toolchain # [win] - # - libpython # [win] + # - gfortran # [win] + # - gcc # [win] # - gfortran # [osx] # only if not installed via homebrew or macports - petsc4py # [unix] - mpi4py # [unix] # only needed if local system has mpi built and is used @@ -14,11 +14,12 @@ dependencies: - jsonmerge - jsonschema - matplotlib - - moorpy>=1.2 + - moorpy==1.2.1 - nlopt - numpy - openmdao - openpyxl + - orbit-nrel>=1.2.5 - pandas - pydoe3 - pyoptsparse @@ -27,8 +28,7 @@ dependencies: - sortedcontainers - statsmodels>=0.14.5 - pip + - windIO>=2.1.1 + - wombat>=0.13.1 - pip: - - orbit-nrel>=1.2.5 - - wombat>=0.13.1 - dearpygui - - windIO diff --git a/environment_dev.yml b/environment_dev.yml index 73f4c8838..b1adb99b3 100644 --- a/environment_dev.yml +++ b/environment_dev.yml @@ -4,21 +4,23 @@ channels: dependencies: # NOTE: uncomment the OS-dependent lines below as needed - # - m2w64-toolchain # [win] - # - libpython # [win] + # - gfortran # [win] + # - gcc # [win] # - gfortran # [osx] # only if not installed via homebrew or macports # - petsc4py # [unix] # - mpi4py # [unix] # only needed if local system has mpi built and is used - cython + - docstring_parser - jsonmerge - jsonschema - matplotlib - - moorpy>=1.2 + - moorpy==1.2.1 - ninja - nlopt - numpy - openmdao - openpyxl + - orbit-nrel>=1.2.5 - pandas - pydoe3 - pyoptsparse @@ -35,13 +37,11 @@ dependencies: - pytest - pytest-cov - pip + - sphinxcontrib-bibtex + - sphinx-copybutton + - sphinx-jsonschema + - sphinx_rtd_theme>=1.3 + - windIO>=2.1.1 + - wombat>=0.13.1 - pip: - - orbit-nrel>=1.2.5 - - wombat>=0.13.1 - dearpygui - - windIO - - sphinxcontrib-bibtex - - sphinx_rtd_theme>=1.3 - - sphinx-jsonschema - - sphinx-copybutton - - docstring-parser diff --git a/examples/02_reference_turbines/IEA-10-198-RWT.yaml b/examples/02_reference_turbines/IEA-10-198-RWT.yaml index 87cdc7360..cd4c93851 100644 --- a/examples/02_reference_turbines/IEA-10-198-RWT.yaml +++ b/examples/02_reference_turbines/IEA-10-198-RWT.yaml @@ -455,7 +455,7 @@ components: start_nd_arc: anchor: name: DP02_DP00_triax - handle: DP02_DP00_uniax + handle: start_nd_arc end_nd_arc: anchor: name: TE diff --git a/examples/03_blade/analysis_options_aero.yaml b/examples/03_blade/analysis_options_aero.yaml index 530d12a19..afd4b35bb 100644 --- a/examples/03_blade/analysis_options_aero.yaml +++ b/examples/03_blade/analysis_options_aero.yaml @@ -18,9 +18,9 @@ design_variables: # corresponding to 'max_efficiency' or 'stall_margin' n_opt: 8 # Number of control points along blade span. During inverse design, # twist is smoothened with a spline with these - # max_decrease: 0.08722222222222221 # Maximum decrease for the twist + # max_decrease: 5 # Maximum decrease for the twist # in [rad] at the n_opt locations. Only used if flag is set to True - # max_increase: 0.08722222222222221 # Maximum increase for the twist + # max_increase: 5 # Maximum increase for the twist # in [rad] at the n_opt locations. Only used if flag is set to True # index_start: 2 # Lock the first two DVs from blade root # index_end: 8 # All DVs close to blade tip are active @@ -40,7 +40,7 @@ constraints: blade: stall: flag: False # Constraint on minimum stall margin - margin: 0.1 # Value of minimum stall margin in [rad] + margin: 5.0 # Value of minimum stall margin in [deg] chord: flag: True # Constraint max chord to its default value (4.75 m) max: 4.75 # Max chord value diff --git a/examples/03_blade/analysis_options_aerostruct.yaml b/examples/03_blade/analysis_options_aerostruct.yaml index cb4090804..6679f1b03 100644 --- a/examples/03_blade/analysis_options_aerostruct.yaml +++ b/examples/03_blade/analysis_options_aerostruct.yaml @@ -13,8 +13,8 @@ design_variables: flag: True # Flag to optimize the twist inverse: False # Flag to determine twist from the user-defined desired margin to stall (defined in constraints) n_opt: 4 # Number of control points along blade span - max_decrease: 0.08722222222222221 # Maximum decrease for the twist in [rad] at the n_opt locations - max_increase: 0.08722222222222221 # Maximum increase for the twist in [rad] at the n_opt locations + max_decrease: 5.0 # Maximum decrease for the twist in [deg] at the n_opt locations + max_increase: 5.0 # Maximum increase for the twist in [deg] at the n_opt locations index_start: 2 # Lock the first two DVs from blade root index_end: 4 # All DVs close to blade tip are active chord: @@ -67,7 +67,7 @@ constraints: margin: 1.4175 stall: flag: True # Constraint on minimum stall margin - margin: 0.087 # Value of minimum stall margin in [rad] + margin: 5.0 # Value of minimum stall margin in [deg] moment_coefficient: flag: True # Constraint on minimum stall margin max: 0.16 diff --git a/examples/03_blade/analysis_options_user.yaml b/examples/03_blade/analysis_options_user.yaml index 2754e6da8..cda382e44 100644 --- a/examples/03_blade/analysis_options_user.yaml +++ b/examples/03_blade/analysis_options_user.yaml @@ -9,8 +9,8 @@ design_variables: flag: True # Flag to optimize the twist inverse: False # Flag to determine twist from the user-defined desired margin to stall (defined in constraints) n_opt: 4 # Number of control points along blade span - max_decrease: 0.08722222222222221 # Maximum decrease for the twist in [rad] at the n_opt locations - max_increase: 0.08722222222222221 # Maximum increase for the twist in [rad] at the n_opt locations + max_decrease: 5.0 # Maximum decrease for the twist in [deg] at the n_opt locations + max_increase: 5.0 # Maximum increase for the twist in [deg] at the n_opt locations index_start: 2 # Lock the first two DVs from blade root index_end: 4 # All DVs close to blade tip are active chord: @@ -42,7 +42,7 @@ constraints: blade: stall: flag: True # Constraint on minimum stall margin - margin: 0.087 # Value of minimum stall margin in [rad] + margin: 5.0 # Value of minimum stall margin in [deg] chord: flag: False # Constraint max chord to its default value (4.75 m) root_circle_diameter: diff --git a/examples/09_floating/IEA-22-280-RWT_Floater.yaml b/examples/09_floating/IEA-22-280-RWT_Floater.yaml index 4be41ad26..e40777ec5 100644 --- a/examples/09_floating/IEA-22-280-RWT_Floater.yaml +++ b/examples/09_floating/IEA-22-280-RWT_Floater.yaml @@ -1118,10 +1118,9 @@ components: generator_rpm_efficiency_user: grid: [1.98, 2.8, 3.47, 3.98, 4.31, 4.49, 5.1, 5.66, 6.39, 7.16] values: [0.7878, 0.9071, 0.941, 0.9526, 0.9566, 0.9582, 0.9614, 0.9622, 0.9608, 0.9541] - elastic_properties_mb: - system_mass: 508040.0 length: 3.047 radius: 4.89 + mass: 508040.0 airfoils: - name: FFA-W3-211 coordinates: @@ -1838,4 +1837,4 @@ control: pitch_actuator_damping: 0.707 yaw_rate: 0.49847328176381617 max_allowable_blade_tip_speed: 95.0 - peak_thrust_shaving: 1.0 \ No newline at end of file + peak_thrust_shaving: 1.0 diff --git a/examples/09_floating/analysis_options_mooropt.yaml b/examples/09_floating/analysis_options_mooropt.yaml index fd2f0cf6f..d9126ec7f 100644 --- a/examples/09_floating/analysis_options_mooropt.yaml +++ b/examples/09_floating/analysis_options_mooropt.yaml @@ -26,9 +26,9 @@ merit_figure: mooring_mass constraints: floating: operational_heel: - upper_bound: 0.08726646259971647 # 5 deg + upper_bound: 5.0 survival_heel: - upper_bound: 0.17453292519943295 # 10 deg + upper_bound: 10.0 max_surge: flag: false upper_bound: 0.1 diff --git a/examples/09_floating/analysis_options_semiopt.yaml b/examples/09_floating/analysis_options_semiopt.yaml index a12a315dd..9e143621d 100644 --- a/examples/09_floating/analysis_options_semiopt.yaml +++ b/examples/09_floating/analysis_options_semiopt.yaml @@ -84,9 +84,9 @@ merit_figure: LCOE constraints: floating: operational_heel: - upper_bound: 0.08726646259971647 # 5 deg + upper_bound: 5.0 survival_heel: - upper_bound: 0.17453292519943295 # 10 deg + upper_bound: 10.0 max_surge: flag: false upper_bound: 0.1 diff --git a/examples/09_floating/analysis_options_sparopt.yaml b/examples/09_floating/analysis_options_sparopt.yaml index 655f2cfff..5439edf0d 100644 --- a/examples/09_floating/analysis_options_sparopt.yaml +++ b/examples/09_floating/analysis_options_sparopt.yaml @@ -28,9 +28,9 @@ merit_figure: platform_mass constraints: floating: operational_heel: - upper_bound: 0.08726646259971647 # 5 deg + upper_bound: 5.0 survival_heel: - upper_bound: 0.17453292519943295 # 10 deg + upper_bound: 10.0 max_surge: flag: false upper_bound: 0.1 diff --git a/examples/13_design_of_experiments/analysis_options.yaml b/examples/13_design_of_experiments/analysis_options.yaml index 826c8d81d..342a5da55 100644 --- a/examples/13_design_of_experiments/analysis_options.yaml +++ b/examples/13_design_of_experiments/analysis_options.yaml @@ -8,8 +8,8 @@ design_variables: flag: False # Flag to optimize the twist inverse: False # Flag to determine twist from the user-defined desired margin to stall (defined in constraints) n_opt: 8 # Number of control points along blade span - max_decrease: 0.08722222222222221 # Maximum decrease for the twist in [rad] at the n_opt locations - max_increase: 0.08722222222222221 # Maximum increase for the twist in [rad] at the n_opt locations + max_decrease: 5.0 # Maximum decrease for the twist in [deg] at the n_opt locations + max_increase: 5.0 # Maximum increase for the twist in [deg] at the n_opt locations chord: flag: True # Flag to optimize the chord n_opt: 8 # Number of control points along blade span diff --git a/examples/16_inverse_design/analysis_options_rotor.yaml b/examples/16_inverse_design/analysis_options_rotor.yaml index 59aa66055..4f9a04087 100644 --- a/examples/16_inverse_design/analysis_options_rotor.yaml +++ b/examples/16_inverse_design/analysis_options_rotor.yaml @@ -9,8 +9,8 @@ design_variables: twist: flag: True # Flag to optimize the twist n_opt: 4 # Number of control points along blade span - max_decrease: 0.08722222222222221 # Maximum decrease for the twist in [rad] at the n_opt locations - max_increase: 0.08722222222222221 # Maximum increase for the twist in [rad] at the n_opt locations + max_decrease: 5.0 # Maximum decrease for the twist in [deg] at the n_opt locations + max_increase: 5.0 # Maximum increase for the twist in [deg] at the n_opt locations index_start: 2 # Lock the first two DVs from blade root index_end: 4 # All DVs close to blade tip are active chord: @@ -95,7 +95,7 @@ constraints: margin: 1.4175 stall: flag: True # Constraint on minimum stall margin - margin: 0.087 # Value of minimum stall margin in [rad] + margin: 5.0 # Value of minimum stall margin in [deg] moment_coefficient: flag: True # Constraint on minimum stall margin max: 0.16 diff --git a/examples/18_rotor_tower_monopile/analysis_options.yaml b/examples/18_rotor_tower_monopile/analysis_options.yaml index dd70808ed..96f314588 100644 --- a/examples/18_rotor_tower_monopile/analysis_options.yaml +++ b/examples/18_rotor_tower_monopile/analysis_options.yaml @@ -9,8 +9,8 @@ design_variables: flag: True # Flag to optimize the twist inverse: False # Flag to determine twist from the user-defined desired margin to stall (defined in constraints) n_opt: 6 # Number of control points along blade span - max_decrease: 0.08722222222222221 # Maximum decrease for the twist in [rad] at the n_opt locations - max_increase: 0.08722222222222221 # Maximum increase for the twist in [rad] at the n_opt locations + max_decrease: 5.0 # Maximum decrease for the twist in [deg] at the n_opt locations + max_increase: 5.0 # Maximum increase for the twist in [deg] at the n_opt locations index_start: 1 # Lock the first DV at blade root index_end: 5 # All DVs close to blade tip are active chord: @@ -71,7 +71,7 @@ constraints: margin: 1.4175 stall: flag: True # Constraint on minimum stall margin - margin: 0.05233 # Value of minimum stall margin in [rad] + margin: 5.0 # Value of minimum stall margin in [deg] moment_coefficient: flag: True max: 0.16 diff --git a/examples/19_rotor_drivetrain_tower/analysis_options.yaml b/examples/19_rotor_drivetrain_tower/analysis_options.yaml index b014c4e3b..c5ec56a70 100644 --- a/examples/19_rotor_drivetrain_tower/analysis_options.yaml +++ b/examples/19_rotor_drivetrain_tower/analysis_options.yaml @@ -13,8 +13,8 @@ design_variables: flag: True inverse: False n_opt: 8 - max_decrease: 0.08722222222222223 - max_increase: 0.08722222222222223 + max_decrease: 5.0 + max_increase: 5.0 index_start: 2 index_end: 8 chord: @@ -82,7 +82,7 @@ constraints: blade: stall: flag: True - margin: 0.05233 + margin: 5.0 chord: flag: True max: 4.3 diff --git a/pyproject.toml b/pyproject.toml index 918fe9b23..bcd32cce6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,10 @@ build-backend = "setuptools.build_meta" [project] name = "wisdem" -version = "4.1.0" +version = "4.1.1" description = "Wind-Plant Integrated System Design & Engineering Model" readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.11" license = {text = "Apache-2.0"} keywords = ["wind", "turbine", "mdao", "design", "optimization"] authors = [ @@ -33,8 +33,6 @@ classifiers = [ # Optional # that you indicate you support Python 3. These classifiers are *not* # checked by "pip install". See instead "python_requires" below. "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -52,11 +50,11 @@ classifiers = [ # Optional dependencies = [ "jsonmerge", "jsonschema", - "moorpy>=1.2", + "moorpy==1.2.1", "numpy", "openmdao", "openpyxl", - "orbit-nrel>=1.2.5", + "orbit-nrel>=1.2.6", "pandas", "pydoe3", "pyyaml", diff --git a/wisdem/__init__.py b/wisdem/__init__.py index dc446aef5..bfb51242b 100644 --- a/wisdem/__init__.py +++ b/wisdem/__init__.py @@ -1 +1,5 @@ +from importlib.metadata import version + from wisdem.glue_code.runWISDEM import run_wisdem + +__version__ = version("wisdem") diff --git a/wisdem/ccblade/ccblade_component.py b/wisdem/ccblade/ccblade_component.py index 7c31c5e04..ede4ed409 100644 --- a/wisdem/ccblade/ccblade_component.py +++ b/wisdem/ccblade/ccblade_component.py @@ -498,7 +498,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): cd = np.zeros(self.n_span) alpha = np.zeros(self.n_span) Emax = np.zeros(self.n_span) - margin2stall = self.options["opt_options"]["constraints"]["blade"]["stall"]["margin"] * 180.0 / np.pi + margin2stall = self.options["opt_options"]["constraints"]["blade"]["stall"]["margin"] Re = np.array(Omega * inputs["r"] * inputs["chord"] * inputs["rho"][0] / inputs["mu"][0]) aoa_op = inputs["aoa_op"] for i in range(self.n_span): @@ -538,7 +538,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): # Cap twist root region to 20 degrees for i in range(len(ccblade.theta)): - cap_twist_root = self.options["opt_options"]["design_variables"]["blade"]["aero_shape"]["twist"]["cap_twist_root"] + cap_twist_root = np.deg2rad(self.options["opt_options"]["design_variables"]["blade"]["aero_shape"]["twist"]["cap_twist_root"]) if ccblade.theta[-i - 1] > cap_twist_root: ccblade.theta[0 : len(ccblade.theta) - i] = cap_twist_root break diff --git a/wisdem/fixed_bottomse/monopile.py b/wisdem/fixed_bottomse/monopile.py index 42dd84aed..02e2322c7 100644 --- a/wisdem/fixed_bottomse/monopile.py +++ b/wisdem/fixed_bottomse/monopile.py @@ -944,7 +944,7 @@ def setup(self): "tower_torsion_modes", "f1", "f2", - "water_depth" + "water_depth", ], ) @@ -1031,3 +1031,4 @@ def setup(self): self.connect("soil.z_k", "monopile.z_soil") self.connect("soil.k", "monopile.k_soil") + self.set_input_defaults("rho_water", 1025.0, units="kg/m**3") diff --git a/wisdem/floatingse/floating.py b/wisdem/floatingse/floating.py index a87d348e2..bb0670f7d 100644 --- a/wisdem/floatingse/floating.py +++ b/wisdem/floatingse/floating.py @@ -146,3 +146,5 @@ def setup(self): for var in ["z_global", "s_full", "s_all"]: self.connect(f"member{k}_{kname}.{var}", f"memload{k}.{var}") + + self.set_input_defaults("rho_water", 1025.0, units="kg/m**3") diff --git a/wisdem/floatingse/floating_frame.py b/wisdem/floatingse/floating_frame.py index 0745b7fa1..eed8113f9 100644 --- a/wisdem/floatingse/floating_frame.py +++ b/wisdem/floatingse/floating_frame.py @@ -698,10 +698,7 @@ def setup(self): n_full = get_nfull(opt["floating"]["members"]["n_height"][k], nref=2) shape = opt["floating"]["members"]["outer_shape"][k] kname = opt['floating']['members']['name'][k] - if shape == "circular": - mem_prom = mem_prom_base + [("cd_usr", f"memload{k}.cd_usr"), ("ca_usr", f"memload{k}.ca_usr")] - elif shape == "rectangular": - mem_prom = mem_prom_base + [("cd_usr", f"memload{k}.cd_usr"), ("cdy_usr", f"memload{k}.cdy_usr"), ("ca_usr", f"memload{k}.ca_usr"), ("cay_usr", f"memload{k}.cay_usr")] + self.add_subsystem( f"memload{k}", MemberLoads( @@ -711,7 +708,7 @@ def setup(self): memmax=True, member_shape=shape, ), - promotes=mem_prom + U_prom + [("joint1", f"member{k}_{kname}:joint1"), ("joint2", f"member{k}_{kname}:joint2")], + promotes= mem_prom_base + U_prom + [("joint1", f"member{k}_{kname}:joint1"), ("joint2", f"member{k}_{kname}:joint2")], ) self.add_subsystem("loadsys", PlatformLoads(options=opt), promotes=["*"]) diff --git a/wisdem/glue_code/gc_LoadInputs.py b/wisdem/glue_code/gc_LoadInputs.py index 0e7ebb703..8f0f37830 100644 --- a/wisdem/glue_code/gc_LoadInputs.py +++ b/wisdem/glue_code/gc_LoadInputs.py @@ -866,6 +866,8 @@ def update_ontology(self, wt_opt): if not self.modeling_options["user_elastic"]["blade"]: # Webs positions TBD # Structural layers + anchors = self.wt_init["components"]["blade"]["structure"]["anchors"] + n_anchors = len(anchors) for i in range(self.modeling_options["WISDEM"]["RotorSE"]["n_layers"]): self.wt_init["components"]["blade"]["structure"]["layers"][i]["thickness"]["grid"] = wt_opt[ "blade.outer_shape.s" @@ -873,6 +875,60 @@ def update_ontology(self, wt_opt): self.wt_init["components"]["blade"]["structure"]["layers"][i]["thickness"]["values"] = wt_opt[ "blade.ps.layer_thickness_param" ][i, :].tolist() + # Update start_nd_arc for the anchor + if "anchor" in self.wt_init["components"]["blade"]["structure"]["layers"][i]["start_nd_arc"]: + anchor_name = self.wt_init["components"]["blade"]["structure"]["layers"][i]["start_nd_arc"]["anchor"]["name"] + anchor_handle = self.wt_init["components"]["blade"]["structure"]["layers"][i]["start_nd_arc"]["anchor"]["handle"] + for j in range(n_anchors): + if anchors[j]["name"] == anchor_name: + self.wt_init["components"]["blade"]["structure"]["anchors"][j][anchor_handle]["grid"] = wt_opt[ + "blade.outer_shape.s" + ].tolist() + self.wt_init["components"]["blade"]["structure"]["anchors"][j][anchor_handle]["values"] = wt_opt[ + "blade.structure.layer_start_nd" + ][i, :].tolist() + break + # Update end_nd_arc for the anchor + if "anchor" in self.wt_init["components"]["blade"]["structure"]["layers"][i]["end_nd_arc"]: + anchor_name = self.wt_init["components"]["blade"]["structure"]["layers"][i]["end_nd_arc"]["anchor"]["name"] + anchor_handle = self.wt_init["components"]["blade"]["structure"]["layers"][i]["end_nd_arc"]["anchor"]["handle"] + for j in range(n_anchors): + if anchors[j]["name"] == anchor_name: + self.wt_init["components"]["blade"]["structure"]["anchors"][j][anchor_handle]["grid"] = wt_opt[ + "blade.outer_shape.s" + ].tolist() + self.wt_init["components"]["blade"]["structure"]["anchors"][j][anchor_handle]["values"] = wt_opt[ + "blade.structure.layer_end_nd" + ][i, :].tolist() + break + for i in range(self.modeling_options["WISDEM"]["RotorSE"]["n_webs"]): + # Update start_nd_arc for the anchor + if "anchor" in self.wt_init["components"]["blade"]["structure"]["webs"][i]["start_nd_arc"]: + anchor_name = self.wt_init["components"]["blade"]["structure"]["webs"][i]["start_nd_arc"]["anchor"]["name"] + anchor_handle = self.wt_init["components"]["blade"]["structure"]["webs"][i]["start_nd_arc"]["anchor"]["handle"] + for j in range(n_anchors): + if anchors[j]["name"] == anchor_name: + self.wt_init["components"]["blade"]["structure"]["anchors"][j][anchor_handle]["grid"] = wt_opt[ + "blade.outer_shape.s" + ].tolist() + self.wt_init["components"]["blade"]["structure"]["anchors"][j][anchor_handle]["values"] = wt_opt[ + "blade.structure.web_start_nd" + ][i, :].tolist() + break + # Update end_nd_arc for the anchor + if "anchor" in self.wt_init["components"]["blade"]["structure"]["webs"][i]["end_nd_arc"]: + anchor_name = self.wt_init["components"]["blade"]["structure"]["webs"][i]["end_nd_arc"]["anchor"]["name"] + anchor_handle = self.wt_init["components"]["blade"]["structure"]["webs"][i]["end_nd_arc"]["anchor"]["handle"] + for j in range(n_anchors): + if anchors[j]["name"] == anchor_name: + self.wt_init["components"]["blade"]["structure"]["anchors"][j][anchor_handle]["grid"] = wt_opt[ + "blade.outer_shape.s" + ].tolist() + self.wt_init["components"]["blade"]["structure"]["anchors"][j][anchor_handle]["values"] = wt_opt[ + "blade.structure.web_end_nd" + ][i, :].tolist() + break + if "elastic_properties" not in self.wt_init["components"]["blade"]["structure"]: self.wt_init["components"]["blade"]["structure"]["elastic_properties"] = {} diff --git a/wisdem/glue_code/gc_PoseOptimization.py b/wisdem/glue_code/gc_PoseOptimization.py index fb197ae77..e3b6ab79c 100644 --- a/wisdem/glue_code/gc_PoseOptimization.py +++ b/wisdem/glue_code/gc_PoseOptimization.py @@ -449,8 +449,8 @@ def set_design_variables(self, wt_opt, wt_init): wt_opt.model.add_design_var( "blade.opt_var.twist_opt", indices=indices_twist, - lower=init_twist_opt[indices_twist] - blade_opt["aero_shape"]["twist"]["max_decrease"], - upper=init_twist_opt[indices_twist] + blade_opt["aero_shape"]["twist"]["max_increase"], + lower=init_twist_opt[indices_twist] - np.deg2rad(blade_opt["aero_shape"]["twist"]["max_decrease"]), + upper=init_twist_opt[indices_twist] + np.deg2rad(blade_opt["aero_shape"]["twist"]["max_increase"]), ) chord_options = blade_opt["aero_shape"]["chord"] @@ -1376,7 +1376,7 @@ def set_initial(self, wt_opt, wt_init): wt_opt["rotorse.rs.constr.max_strainU_te"] = blade_constr["strains_te_ss"]["max"] wt_opt["rotorse.rs.constr.max_strainL_te"] = blade_constr["strains_te_ps"]["max"] - wt_opt["rotorse.stall_check.stall_margin"] = blade_constr["stall"]["margin"] * 180.0 / np.pi + wt_opt["rotorse.stall_check.stall_margin"] = blade_constr["stall"]["margin"] if self.modeling["flags"]["tower"]: wt_opt["tcons.max_allowable_td_ratio"] = blade_constr["tip_deflection"]["margin"] @@ -1384,10 +1384,10 @@ def set_initial(self, wt_opt, wt_init): drive_constr = self.opt["constraints"]["drivetrain"] wt_opt["drivese.shaft_deflection_allowable"] = drive_constr["shaft_deflection"]["upper_bound"] - wt_opt["drivese.shaft_angle_allowable"] = drive_constr["shaft_angle"]["upper_bound"] + wt_opt["drivese.shaft_angle_allowable"] = np.deg2rad(drive_constr["shaft_angle"]["upper_bound"]) wt_opt["drivese.stator_deflection_allowable"] = drive_constr["stator_deflection"]["upper_bound"] - wt_opt["drivese.stator_angle_allowable"] = drive_constr["stator_angle"]["upper_bound"] + wt_opt["drivese.stator_angle_allowable"] = np.deg2rad(drive_constr["stator_angle"]["upper_bound"]) if self.modeling["WISDEM"]["DriveSE"]["direct"]: wt_opt["drivese.access_diameter"] = drive_constr["access"]["lower_bound"] diff --git a/wisdem/glue_code/gc_WT_DataStruc.py b/wisdem/glue_code/gc_WT_DataStruc.py index 4e97a3c31..a27bd8022 100644 --- a/wisdem/glue_code/gc_WT_DataStruc.py +++ b/wisdem/glue_code/gc_WT_DataStruc.py @@ -2282,12 +2282,6 @@ def compute(self, inputs, outputs): locations = inputs["location"] joints_xyz = NULL * np.ones(outputs["joints_xyz"].shape) - # Handle cylindrical coordinate joints - icyl = floating_init_options["joints"]["cylindrical"] - locations_xyz = locations.copy() - locations_xyz[icyl, 0] = locations[icyl, 0] * np.cos(np.deg2rad(locations[icyl, 1])) - locations_xyz[icyl, 1] = locations[icyl, 0] * np.sin(np.deg2rad(locations[icyl, 1])) - # Handle relative joints joint_names = floating_init_options["joints"]["name"] for i_joint in range(floating_init_options["joints"]["n_joints"]): @@ -2298,11 +2292,18 @@ def compute(self, inputs, outputs): f"The relative joint {joint_names[i_joint]} is not relative to an existing joint. Relative joint provided: {rel_joint}" ) - rel_joint_location = locations_xyz[name2idx[rel_joint]] + rel_joint_location = locations[name2idx[rel_joint]] relative_dimensions = np.array( floating_init_options["joints"]["relative_dims"][i_joint] ) # These joints are relative - locations_xyz[i_joint][relative_dimensions] += rel_joint_location[relative_dimensions] + locations[i_joint][relative_dimensions] += rel_joint_location[relative_dimensions] + + # Handle cylindrical coordinate joints + icyl = floating_init_options["joints"]["cylindrical"] + locations_xyz = locations.copy() + locations_xyz[icyl, 0] = locations[icyl, 0] * np.cos(np.deg2rad(locations[icyl, 1])) + locations_xyz[icyl, 1] = locations[icyl, 0] * np.sin(np.deg2rad(locations[icyl, 1])) + joints_xyz[:n_joints, :] = locations_xyz.copy() diff --git a/wisdem/inputs/analysis_schema.yaml b/wisdem/inputs/analysis_schema.yaml index c614cb76c..01f39e703 100644 --- a/wisdem/inputs/analysis_schema.yaml +++ b/wisdem/inputs/analysis_schema.yaml @@ -58,7 +58,7 @@ properties: twist: type: object default: {} - description: Blade twist as a design variable by adding or subtracting radians from the initial value at spline control points along the span. + description: Blade twist as a design variable by adding or subtracting degrees from the initial value at spline control points along the span. properties: flag: *flag inverse: @@ -78,13 +78,13 @@ properties: max_decrease: type: number description: Maximum allowable decrease of twist at each DV location along blade span. - default: 0.1 - unit: rad + default: 5.0 + unit: deg max_increase: type: number description: Maximum allowable increase of twist at each DV location along blade span. - default: 0.1 - unit: rad + default: 5.0 + unit: deg index_start: &index_start type: integer default: 0 @@ -100,8 +100,8 @@ properties: cap_twist_root: type: number description: Maximum allowable twist during an inverse design. Close to blade root, twist will be capped to this value. - default: 0.349 - unit: rad + default: 20.0 + unit: deg chord: type: object default: {} @@ -194,9 +194,9 @@ properties: lower_bound: &angbound type: number minimum: 0.0 - maximum: 0.5235987756 # 30 deg + maximum: 30.0 default: 0.0 - unit: rad + unit: deg description: Design variable bound upper_bound: *angbound hub_diameter: @@ -733,18 +733,18 @@ properties: properties: lower_bound: type: number - unit: rad + unit: deg description: Design variable bound default: 0.0 minimum: 0.0 - maximum: 3.141592653589793 #180 deg + maximum: 180.0 #180 deg upper_bound: type: number - unit: rad + unit: deg description: Design variable bound - default: 0.1 + default: 5.0 minimum: 0.0 - maximum: 3.141592653589793 #180 deg + maximum: 180.0 #180 deg mooring: type: object description: Design variables associated with the mooring system @@ -931,10 +931,10 @@ properties: flag: *flag margin: type: number - default: 0.05233 # 3 deg + default: 5.0 # 5 deg minimum: 0.0 - maximum: 0.5 - unit: radians + maximum: 30.0 + unit: deg chord: type: object description: Enforcing the maximum chord length limit at all points along blade span. @@ -1288,8 +1288,8 @@ properties: type: number default: 1e-3 minimum: 1e-5 - maximum: 1.0 - unit: radian + maximum: 30.0 + unit: deg description: Upper limit of angular deflection stator_deflection: type: object @@ -1301,7 +1301,7 @@ properties: stator_angle: type: object default: {} - description: Allowable non-torque angular deflection of the nose or bedplate, in radians, at the generator stator attachment + description: Allowable non-torque angular deflection of the nose or bedplate, in degrees, at the generator stator attachment properties: flag: *flag upper_bound: *angular_upper @@ -1322,10 +1322,10 @@ properties: properties: upper_bound: &upang type: number - default: 0.17453292519943295 # 10 deg - minimum: 0.017453292519943295 # 1 deg - maximum: 0.7853981633974483 # 45 deg - unit: rad + default: 10.0 + minimum: 1.0 + maximum: 45.0 + unit: deg survival_heel: *heelang max_surge: type: object diff --git a/wisdem/inputs/modeling_schema.yaml b/wisdem/inputs/modeling_schema.yaml index 77f4036ea..a7e952a3e 100644 --- a/wisdem/inputs/modeling_schema.yaml +++ b/wisdem/inputs/modeling_schema.yaml @@ -73,7 +73,7 @@ properties: description: Min pitch angle of the Cp-Ct-Cq-surfaces max_pitch_perf_surfaces: type: number - default: 30. + default: 40. description: Max pitch angle of the Cp-Ct-Cq-surfaces n_tsr_perf_surfaces: type: integer @@ -500,21 +500,18 @@ properties: n_ctv: type: number description: Number of crew transfer vessels (offshore) or onsite trucks (land-based) that should be made available to the wind farm. - units: unitless minimum: 1 maximum: 20 default: 3 n_hlv: type: number description: Number of heavy lift vessels (fixed-bottom offshore) or crawler cranes (land-based) that should be made available to the wind farm. - units: unitless minimum: 1 maximum: 10 default: 1 n_tugboat: type: number description: Number of tugboat groups that should be available to the port to tow floating turbines to port and back. - units: unitless minimum: 1 maximum: 10 default: 2 @@ -535,14 +532,12 @@ properties: n_port_crews: type: number description: Number of port-side crews available to work on simultaneous repairs for any at-port turbine. - units: unitless minimum: 1 maximum: 100 default: 2 max_port_operations: type: number description: Number of turbines that can be at port at once. - units: unitless minimum: 1 maximum: 100 default: 2 @@ -556,27 +551,22 @@ properties: maintenance_start: type: string description: Starting date, in MM/DD format; year will be inserted automatically based on input to `years`. - units: unitless default: "none" non_operational_start: type: string description: Starting date, in MM/DD format, for an annual period where the site is inaccessible. - units: string default: "none" non_operational_end: type: string description: Ending date, in MM/DD format, for an annual period where the site is inaccessible. - units: unitless default: "none" reduced_speed_start: type: string description: Starting date, in MM/DD format, for an annual period where traveling speed is reduced. - units: unitless default: "none" reduced_speed_end: type: string description: Ending date, in MM/DD format, for an annual period where traveling speed is reduced. - units: unitless default: "none" reduced_speed: type: number @@ -588,7 +578,6 @@ properties: random_seed: type: number description: Random seed for the internal random generator. - units: "none" minimum: 1 maximum: 4294967295 default: 42 diff --git a/wisdem/orbit/orbit_api.py b/wisdem/orbit/orbit_api.py index 5eb3a9809..8149e8073 100644 --- a/wisdem/orbit/orbit_api.py +++ b/wisdem/orbit/orbit_api.py @@ -805,4 +805,7 @@ def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): outputs["installation_time"] = project.installation_time outputs["installation_capex"] = project.installation_capex outputs["capacity"] = project.capacity - discrete_outputs["layout"] = project.phases["ArraySystemDesign"].create_layout_df() + if "ArraySystemDesign" in project.phases: + discrete_outputs["layout"] = project.phases["ArraySystemDesign"].create_layout_df() + elif "CustomArraySystemDesign" in project.phases: + discrete_outputs["layout"] = project.phases["CustomArraySystemDesign"].create_layout_df() diff --git a/wisdem/postprocessing/wisdem_get.py b/wisdem/postprocessing/wisdem_get.py index 6954ece18..93f30d14c 100644 --- a/wisdem/postprocessing/wisdem_get.py +++ b/wisdem/postprocessing/wisdem_get.py @@ -232,18 +232,21 @@ def get_nacelle_mass(prob): # 'MoI_cm_xx', 'MoI_cm_yy', 'MoI_cm_zz', 'MoI_cm_xy', 'MoI_cm_xz', 'MoI_cm_yz', # 'MoI_TT_xx', 'MoI_TT_yy', 'MoI_TT_zz', 'MoI_TT_xy', 'MoI_TT_xz', 'MoI_TT_yz'] nacDF = prob.model.wt.wt_rna.drivese.nac._mass_table - hub_cm = prob["drivese.hub_system_cm"][0] + hub_cm_in = prob["drivese.hub_system_cm"][0] L_drive = prob["drivese.L_drive"][0] tilt = prob.get_val('drivetrain.uptilt', 'rad')[0] shaft0 = prob["drivese.shaft_start"] - Cup = -1.0 - hub_cm = R = shaft0 + (L_drive + hub_cm) * np.array([Cup * np.cos(tilt), 0.0, np.sin(tilt)]) + Cup = -1.0 + cm_array = np.array([Cup * np.cos(tilt), 0.0, np.sin(tilt)]) + hub_cm = R = shaft0 + (L_drive + hub_cm_in) * cm_array hub_mass = prob['drivese.hub_system_mass'] hub_I = prob["drivese.hub_system_I"] hub_I_TT = util.rotateI(hub_I, -Cup * tilt, axis="y") hub_I_TT = util.unassembleI( util.assembleI(hub_I_TT) + hub_mass * (np.dot(R, R) * np.eye(3) - np.outer(R, R)) ) + blades_cm_in = prob["drivese.blades_cm"][0] blades_mass = prob['drivese.blades_mass'] + blades_cm = R = shaft0 + (L_drive + hub_cm_in + blades_cm_in) * cm_array blades_I = prob["drivese.blades_I"] blades_I_TT = util.rotateI(blades_I, -Cup * tilt, axis="y") blades_I_TT = util.unassembleI( util.assembleI(blades_I_TT) + @@ -251,11 +254,13 @@ def get_nacelle_mass(prob): rna_mass = prob['drivese.rna_mass'] rna_cm = R = prob['drivese.rna_cm'] rna_I_TT = prob['drivese.rna_I_TT'] - rna_I = util.unassembleI( util.assembleI(rna_I_TT) + - rna_mass * (np.dot(R, R) * np.eye(3) + np.outer(R, R)) ) - nacDF.loc['Blades'] = np.r_[blades_mass, hub_cm, blades_I, blades_I_TT].tolist() + # Moving from TT-coord sys back to CoM, so subtract. + rna_I = util.unassembleI( util.assembleI(rna_I_TT) - + rna_mass * (np.dot(R, R) * np.eye(3) - np.outer(R, R)) ) + nacDF.loc['Blades'] = np.r_[blades_mass, blades_cm, blades_I, blades_I_TT].tolist() nacDF.loc['Hub_System'] = np.r_[hub_mass, hub_cm, hub_I, hub_I_TT].tolist() nacDF.loc['RNA'] = np.r_[rna_mass, rna_cm, rna_I, rna_I_TT].tolist() + return nacDF diff --git a/wisdem/test/test_floatingse/test_relative_joints.py b/wisdem/test/test_floatingse/test_relative_joints.py new file mode 100644 index 000000000..f0f282d30 --- /dev/null +++ b/wisdem/test/test_floatingse/test_relative_joints.py @@ -0,0 +1,166 @@ +""" +Unit tests for AggregateJoints with relative joints in rectangular/cylindrical coordinates. + +Tests that mooring anchor joints defined relative to column keel joints +are resolved to the correct Cartesian (XYZ) positions. +Test that a heave plate relative to a column keel is resolved correctly in the z dimension but not x/y. +""" + +import unittest + +import numpy as np +import openmdao.api as om + +from wisdem.glue_code.gc_WT_DataStruc import AggregateJoints + + +def make_floating_init_options(joint_names, cylindrical, relative, relative_dims): + """Build a minimal floating_init_options dict for AggregateJoints with no members.""" + n_joints = len(joint_names) + name2idx = {name: i for i, name in enumerate(joint_names)} + return { + "joints": { + "n_joints": n_joints, + "name": joint_names, + "cylindrical": cylindrical, + "relative": relative, + "relative_dims": relative_dims, + "name2idx": name2idx, + }, + "members": { + "n_members": 0, + "name": [], + "joint1": [], + "joint2": [], + "n_axial_joints": [], + "no_intersect": [], + }, + } + + +class TestRelativeJoints(unittest.TestCase): + """Test AggregateJoints resolves relative joints correctly in both cylindrical and Cartesian coordinates.""" + + @staticmethod + def _run_aggregate_joints(floating_init_options, locations_input): + prob = om.Problem() + prob.model.add_subsystem( + "alljoints", + AggregateJoints(floating_init_options=floating_init_options), + promotes=["*"], + ) + prob.setup() + prob["location"] = locations_input + prob.run_model() + return prob["joints_xyz"] + + def test_single_relative_cylindrical_joint(self): + """ + anchor1 is relative to col_keel in the radial dimension only. + After relative offset: anchor1 r = r_anchor_offset + r_col + After cyl->XYZ: x = r_total * cos(theta), y = r_total * sin(theta) + """ + r_col = 51.75 + theta_col = 180.0 + z_keel = -20.0 + r_anchor_offset = 786.05 + z_anchor = -200.0 + + joint_names = ["col_keel", "anchor1"] + locations = np.array([ + [r_col, theta_col, z_keel], # col_keel (cylindrical) + [r_anchor_offset, theta_col, z_anchor], # anchor1 (cylindrical, relative r) + ]) + cylindrical = [True, True] + relative = ["origin", "col_keel"] + relative_dims = [[False, False, False], [True, False, False]] + + opts = make_floating_init_options(joint_names, cylindrical, relative, relative_dims) + joints_xyz = self._run_aggregate_joints(opts, locations.copy()) + + # col_keel XYZ + np.testing.assert_allclose(joints_xyz[0, 0], r_col * np.cos(np.deg2rad(theta_col)), atol=1e-10) + np.testing.assert_allclose(joints_xyz[0, 1], r_col * np.sin(np.deg2rad(theta_col)), atol=1e-10) + np.testing.assert_allclose(joints_xyz[0, 2], z_keel, atol=1e-10) + + # anchor1 XYZ — relative r resolved before cylindrical conversion + r_total = r_anchor_offset + r_col + np.testing.assert_allclose(joints_xyz[1, 0], r_total * np.cos(np.deg2rad(theta_col)), atol=1e-10) + np.testing.assert_allclose(joints_xyz[1, 1], r_total * np.sin(np.deg2rad(theta_col)), atol=1e-10) + np.testing.assert_allclose(joints_xyz[1, 2], z_anchor, atol=1e-10) + + def test_three_mooring_anchors_iea15mw(self): + """ + Reproduce the IEA-15-240-RWT VolturnUS-S mooring anchor geometry. + Column keels at 120-deg spacing; anchors at relative radial offset from each keel. + """ + r_col = 51.75 + z_keel = -20.0 + r_anchor_offset = 786.05 + z_anchor = -200.0 + thetas = [180.0, 60.0, -60.0] + + joint_names = ["col1_keel", "col2_keel", "col3_keel", "anchor1", "anchor2", "anchor3"] + locations = np.array([ + [r_col, thetas[0], z_keel], + [r_col, thetas[1], z_keel], + [r_col, thetas[2], z_keel], + [r_anchor_offset, thetas[0], z_anchor], + [r_anchor_offset, thetas[1], z_anchor], + [r_anchor_offset, thetas[2], z_anchor], + ]) + cylindrical = [True] * 6 + relative = ["origin", "origin", "origin", "col1_keel", "col2_keel", "col3_keel"] + relative_dims = [[False, False, False]] * 3 + [[True, False, False]] * 3 + + opts = make_floating_init_options(joint_names, cylindrical, relative, relative_dims) + joints_xyz = self._run_aggregate_joints(opts, locations.copy()) + + r_total = r_anchor_offset + r_col + for i, (theta, name) in enumerate(zip(thetas, ["anchor1", "anchor2", "anchor3"])): + idx = i + 3 # anchors start at index 3 + np.testing.assert_allclose(joints_xyz[idx, 0], r_total * np.cos(np.deg2rad(theta)), atol=1e-10, + err_msg=f"{name} x mismatch") + np.testing.assert_allclose(joints_xyz[idx, 1], r_total * np.sin(np.deg2rad(theta)), atol=1e-10, + err_msg=f"{name} y mismatch") + np.testing.assert_allclose(joints_xyz[idx, 2], z_anchor, atol=1e-10, + err_msg=f"{name} z mismatch") + + def test_cartesian_relative_to_cylindrical_parent(self): + """ + heave_plate_base is a Cartesian joint positioned below col_keel. + + The relative offset uses the raw location of col_keel (before cyl->XYZ), + so only the z dimension is inherited: heave_plate_base.z = z_offset + z_keel. + x and y are unchanged (absolute Cartesian). + """ + r_col = 51.75 + theta_col = 180.0 + z_keel = -20.0 + z_offset = -0.25 + + joint_names = ["col_keel", "heave_plate_base"] + locations = np.array([ + [r_col, theta_col, z_keel], # col_keel (cylindrical) + [0.0, 0.0, z_offset], # heave_plate_base (Cartesian, relative z) + ]) + cylindrical = [True, False] + relative = ["origin", "col_keel"] + relative_dims = [[False, False, False], [False, False, True]] + + opts = make_floating_init_options(joint_names, cylindrical, relative, relative_dims) + joints_xyz = self._run_aggregate_joints(opts, locations.copy()) + + # col_keel XYZ (from cylindrical) + np.testing.assert_allclose(joints_xyz[0, 0], r_col * np.cos(np.deg2rad(theta_col)), atol=1e-10) + np.testing.assert_allclose(joints_xyz[0, 1], r_col * np.sin(np.deg2rad(theta_col)), atol=1e-10) + np.testing.assert_allclose(joints_xyz[0, 2], z_keel, atol=1e-10) + + # heave_plate_base: x, y unchanged; z = z_offset + z_keel + np.testing.assert_allclose(joints_xyz[1, 0], 0.0, atol=1e-10) + np.testing.assert_allclose(joints_xyz[1, 1], 0.0, atol=1e-10) + np.testing.assert_allclose(joints_xyz[1, 2], z_offset + z_keel, atol=1e-10) + + +if __name__ == "__main__": + unittest.main() diff --git a/wisdem/test/test_rotorse/test_rotor_power.py b/wisdem/test/test_rotorse/test_rotor_power.py index 117a5d372..b04602141 100644 --- a/wisdem/test/test_rotorse/test_rotor_power.py +++ b/wisdem/test/test_rotorse/test_rotor_power.py @@ -15,6 +15,8 @@ def fillprob(prob, n_pc, n_span): prob.setup() for k in NPZFILE.files: + if k in ["generator_efficiency","lss_rpm"]: + continue prob[k] = NPZFILE[k] prob.set_val("v_min", 4.0, units="m/s")