From 5f63820adf7b2a91e7c08a50da2dc19436ae4694 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20A=2E=20Michel=C3=A9n=20Str=C3=B6fer?= Date: Mon, 1 Apr 2024 09:54:01 -0600 Subject: [PATCH 01/27] Update CONTRIBUTING.md to indicate PRs should be to the new `dev` branch From dfd63aba8db6e4473170ffb3d9febfce9a95d91b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20A=2E=20Michel=C3=A9n=20Str=C3=B6fer?= Date: Mon, 1 Apr 2024 09:57:08 -0600 Subject: [PATCH 02/27] Update RELEASING.md to reflect new workflow with the `dev` branch From 51d85a90d6ec5e0ba9f4d4b36028f1cb7cf3a7ac Mon Sep 17 00:00:00 2001 From: Ryan Coe Date: Tue, 2 Apr 2024 14:03:02 -0600 Subject: [PATCH 03/27] update docstrings (#326) From 44cb872bbe8137188f4d57f052e44ea63bbe7f53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20A=2E=20Michel=C3=A9n=20Str=C3=B6fer?= Date: Tue, 2 Apr 2024 15:26:11 -0600 Subject: [PATCH 04/27] damping naming and consistently change radiation damping (#328) From 7fc69020eacf440d7c672fe1cac188561c5b87ce Mon Sep 17 00:00:00 2001 From: Ryan Coe Date: Wed, 3 Apr 2024 08:20:39 -0600 Subject: [PATCH 05/27] actually test power solution for irregular wave (#327) --- tests/test_integration.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_integration.py b/tests/test_integration.py index 9e60a1150..739956d0d 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -363,7 +363,7 @@ def test_unstructured_controller_long_crested_wave(self, Fex = res_fd[0].force.sel( type=['Froude_Krylov', 'diffraction']).sum('type') power_optimal = (np.abs(Fex)**2/8 / np.real(hydro_impedance.squeeze()) - ).squeeze().sum('omega').item() + ).squeeze().sum('omega').item() assert power_sol == approx(power_optimal, rel=1e-2) From 7f03d9b8339924ff751fb89fcaa0aa1573f3b14d Mon Sep 17 00:00:00 2001 From: Daniel Gaebele <86246113+dtgaebe@users.noreply.github.com> Date: Wed, 3 Apr 2024 14:00:35 -0700 Subject: [PATCH 06/27] issue 321 fd_to_td() bug (#329) * bug bix : DC and Nyquist frequency should not be devided by two before ifft * Changed td_to_fd to scale single sided frequency components rather than TD signal * minor bug fix from issue332 #332 From 92d5710a58e84f218e02b1b01302ddbb7878412f Mon Sep 17 00:00:00 2001 From: Ryan Coe Date: Wed, 3 Apr 2024 15:27:45 -0600 Subject: [PATCH 07/27] nodf -> ndof (#334) From a5920e8aa3d45c1445228bbb4e939dbd95c2b17e Mon Sep 17 00:00:00 2001 From: Ryan Coe Date: Wed, 3 Apr 2024 15:42:05 -0600 Subject: [PATCH 08/27] add DOI for Daniel's paper (#336) From 35ad528dedd86b2a9ef7a1c1365ace20dc42edae Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Mon, 6 May 2024 19:17:44 -0600 Subject: [PATCH 09/27] Lower tolerance for new test to fix CI failing occasionally From aa128ea1a090de834855db9b9b2e834fd27efd4f Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Wed, 8 May 2024 14:00:26 -0600 Subject: [PATCH 10/27] hyperlinks no longer have formatting, plus other small adjustments (#348) From d1e7af1102e8b32f090eb32ee5367f1dc860ec04 Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Thu, 9 May 2024 08:36:37 -0600 Subject: [PATCH 11/27] Merge to dev, not main (#349) From dc69497de0b3eba3bff33e7c9a87f137c487dbc8 Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Tue, 14 May 2024 21:09:19 -0400 Subject: [PATCH 12/27] Dev version of documentation site (#347) * added initial file changes based on sphinx_multiversion docs and WEC-Sim implementation * removed sphinx-multiversion since it is no longer supported and made manual multiversion * now uses absolute paths, commented out linkcheck for debugging * fixed docstring errors in utilities module * updating files again that somehow got reverted * fixing path in conf.py * don't run tutorials (will revert later) * handle file moves correctly, fixed if statement to make other versions appear * fixed two bugs in versions template * reverted temp changes, changes latest to main * switched latest to main * main branch now in root directory of pages * fixed URLs with change from last commit * make other branches visible before building * switched main branch tag for more testing * fixed typo * switched dev branch to an existing branch * renamed main to latest, changed version.html file name to avoid confusion * added prints about moving files so Sphinx output isn't misleading * fixed typo with quotations * changed versions.html name back because that broke things I guess * modified contributing documentation to reflect changes * add logic to remove duplicate 'latest' branch * Fixed pathing when already on latest * remove typo * Troubleshooting complete, switching back to correct branches for deployment * Removed extra word in docstring * removed redundant function * fixed pathing so returns to same file (and fixes tutorial/API docs) * changed latest branch for demonstration * switched back latest branch for deployment From a5c26dc683096766b9899a30d5a5ad35558fed77 Mon Sep 17 00:00:00 2001 From: mcdevin Date: Wed, 15 May 2024 06:59:38 -0600 Subject: [PATCH 13/27] updated with new Capytaine docs URL From 0f2000cbf62becc5d675f217d7458068f34311a4 Mon Sep 17 00:00:00 2001 From: jtgrasb <87095491+jtgrasb@users.noreply.github.com> Date: Wed, 15 May 2024 11:07:44 -0600 Subject: [PATCH 14/27] Add warnings when adding inertia and hydrostatic stiffness automatically (#346) From 5988d9f8d3602cffc7df9de5f8c3a82ba44f3f01 Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Thu, 13 Jun 2024 13:33:04 -0600 Subject: [PATCH 15/27] CI workflow cleanup (#352) * removed conda environment from workflows since newer capytaine/wavespectra work with Windows * fixed unnecessary capitalization * still create CI conda environment to fix Mac environment failures * added conda env fully back in, push workflow deploys docs, split PR workflow * conda environment activates again * mambaforge instead of miniforge * manual cache reset * reset to older version of setup-miniconda to troubleshoot From ab6ad78e1a69499a913e7e0af9f269a002570bbc Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Fri, 21 Jun 2024 11:45:04 -0600 Subject: [PATCH 16/27] Specify NumPy version <2.0 since it breaks autograd --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 824dfd1f7..807710977 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "wecopttool" -version = "3.0.1" +version = "3.0.0" description = "WEC Design Optimization Toolbox" readme = "README.md" authors = [ From 0b9304563ff3f670f77c0b6fdb61441eb170cf35 Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Fri, 2 Aug 2024 10:23:49 -0600 Subject: [PATCH 17/27] remove realization dimension if it exists (#365) --- wecopttool/core.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/wecopttool/core.py b/wecopttool/core.py index 10a97cd9d..40ca2a1be 100644 --- a/wecopttool/core.py +++ b/wecopttool/core.py @@ -758,6 +758,11 @@ def solve(self, _log.info("Solving pseudo-spectral control problem " + f"for realization number {realization}.") + try: + wave = wave.squeeze(dim='realization') + except KeyError: + pass + # objective function sign = -1.0 if maximize else 1.0 From 9e55214a9d190c4e0d37d31ae6dbe0a5e3380b6d Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Fri, 2 Aug 2024 15:43:21 -0600 Subject: [PATCH 18/27] Update pyproject.toml --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 807710977..9b9440e73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "wecopttool" -version = "3.0.0" +version = "3.0.2" description = "WEC Design Optimization Toolbox" readme = "README.md" authors = [ From 362b78c219b0977c61fcdf27a3b624ed8826287f Mon Sep 17 00:00:00 2001 From: jtgrasb <87095491+jtgrasb@users.noreply.github.com> Date: Tue, 13 Aug 2024 13:09:27 -0600 Subject: [PATCH 19/27] Pioneer tutorial updates (empirical impedance model and nonlinear spring) (#362) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update CONTRIBUTING.md to indicate PRs should be to the new `dev` branch * Update RELEASING.md to reflect new workflow with the `dev` branch * Set Default uniform_shift to False (#313) * Make uniform shift default false and fix test_core * Allow for non-uniform shift for impedance * update docstrings (#326) * damping naming and consistently change radiation damping (#328) * run CI + codeQL on dev PRs/pushses * Fix test failures (#330) * fix tests * fix pioneer * actually test power solution for irregular wave (#327) * Update test_integration with new scaling (#333) * Phase Realizations Demo and Docs (#315) * Pioneer tutorial realizations demo Add frequency array and realization study to beginning of pioneer tutorial * Update pioneer * Clean pioneer and update docs * remove print * Units * Update pioneer plots * Waves before frequencies * Explain less frequencies * Minor updates * Update pioneer * Merge branch 'dev' of https://github.com/sandialabs/WecOptTool into phases_demo * issue 321 fd_to_td() bug (#329) * bug bix : DC and Nyquist frequency should not be devided by two before ifft * Changed td_to_fd to scale single sided frequency components rather than TD signal * minor bug fix from issue332 #332 * nodf -> ndof (#334) * add DOI for Daniel's paper (#336) * Tutorial cleanup (#339) * run CI on PRs against dev branch * revamped tutorial 1, including fix for #293 * more tutorial cleanup and editorial changes * more cleanup and incorporated changes in #315 * fixed tutorial 2 colormaps * finishing touches * reverted a few accidental changes * fixes as per Jeff's review comments --------- Co-authored-by: Ryan Coe * Utils dev - replaces old utilities PR (#343) * run CI on PRs against dev branch * coppied fundamental utility files * import utilities module * added utilities funtions to tut1 * added bem plot from utils * added bem plot from utils * updated sankey plot * updated check_radiation_damping * cleared outputs * corrected bug * changed Zi to hydro_impedance to be consistent with our variables name python convention * PR review edits * add grid to plots * removed draft functions in utilities.py * typo * Fixed one more typo I found while reviewing Daniel's changes --------- Co-authored-by: Ryan Coe Co-authored-by: Michael Devin * Post process docstrings; use loops for multiple realizations (#337) * post_processing docstrings - examples - parameters (order) * handle multiple phase realizations internally * Update wecopttool/core.py * making outputs lists * Update implementation to function with tutorial 1 for now * Update tutorials * Update LUPA * Make sure same WEC is passed in * Add test_utilities Revert "Add test_utilities" This reverts commit 27399f03320f2c69fcb641e1496521b40c4a9d0b. * Update utilities module * Update utilities * Update test_utilities * Update tutorial 1 utilities call --------- Co-authored-by: Carlos A. Michelén Ströfer Co-authored-by: Carlos A. Michelén Ströfer Co-authored-by: jtgrasb Co-authored-by: jtgrasb <87095491+jtgrasb@users.noreply.github.com> * v3.0.0 * Lower tolerance for new test to fix CI failing occasionally * hyperlinks no longer have formatting, plus other small adjustments (#348) * Merge to dev, not main (#349) * Dev version of documentation site (#347) * added initial file changes based on sphinx_multiversion docs and WEC-Sim implementation * removed sphinx-multiversion since it is no longer supported and made manual multiversion * now uses absolute paths, commented out linkcheck for debugging * fixed docstring errors in utilities module * updating files again that somehow got reverted * fixing path in conf.py * don't run tutorials (will revert later) * handle file moves correctly, fixed if statement to make other versions appear * fixed two bugs in versions template * reverted temp changes, changes latest to main * switched latest to main * main branch now in root directory of pages * fixed URLs with change from last commit * make other branches visible before building * switched main branch tag for more testing * fixed typo * switched dev branch to an existing branch * renamed main to latest, changed version.html file name to avoid confusion * added prints about moving files so Sphinx output isn't misleading * fixed typo with quotations * changed versions.html name back because that broke things I guess * modified contributing documentation to reflect changes * add logic to remove duplicate 'latest' branch * Fixed pathing when already on latest * remove typo * Troubleshooting complete, switching back to correct branches for deployment * Removed extra word in docstring * removed redundant function * fixed pathing so returns to same file (and fixes tutorial/API docs) * changed latest branch for demonstration * switched back latest branch for deployment * updated with new Capytaine docs URL * Add warnings when adding inertia and hydrostatic stiffness automatically (#346) * Add data and update tutorial 4 * CI workflow cleanup (#352) * removed conda environment from workflows since newer capytaine/wavespectra work with Windows * fixed unnecessary capitalization * still create CI conda environment to fix Mac environment failures * added conda env fully back in, push workflow deploys docs, split PR workflow * conda environment activates again * mambaforge instead of miniforge * manual cache reset * reset to older version of setup-miniconda to troubleshoot * Specify NumPy version <2.0 since it breaks autograd * v3.0.1 (#360) * Revert "v3.0.1 (#360)" (#361) This reverts commit f79ff8b97bde9c674b921a526ce261db4f8f299b. * Update pyproject.toml * Update pioneer tutorial with nl spring and empirical model * Clear outputs * Consolidate empirical data and fix small issues * Remove consolidation file --------- Co-authored-by: Carlos A. Michelén Ströfer Co-authored-by: Ryan Coe Co-authored-by: Daniel Gaebele <86246113+dtgaebe@users.noreply.github.com> Co-authored-by: Michael Devin Co-authored-by: Carlos A. Michelén Ströfer Co-authored-by: mcdevin --- examples/data/pioneer_empirical_data.nc | Bin 0 -> 18608 bytes examples/tutorial_4_Pioneer.ipynb | 627 +++++++++++++++--------- wecopttool/utilities.py | 38 +- 3 files changed, 427 insertions(+), 238 deletions(-) create mode 100644 examples/data/pioneer_empirical_data.nc diff --git a/examples/data/pioneer_empirical_data.nc b/examples/data/pioneer_empirical_data.nc new file mode 100644 index 0000000000000000000000000000000000000000..d4a0c81541fa00d0a75e6e39746d596f1d4ed675 GIT binary patch literal 18608 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 literal 0 HcmV?d00001 diff --git a/examples/tutorial_4_Pioneer.ipynb b/examples/tutorial_4_Pioneer.ipynb index d7aa24dcd..45cc13b2f 100644 --- a/examples/tutorial_4_Pioneer.ipynb +++ b/examples/tutorial_4_Pioneer.ipynb @@ -38,6 +38,8 @@ "import autograd.numpy as np\n", "import matplotlib.pyplot as plt\n", "from scipy.linalg import block_diag\n", + "import xarray as xr\n", + "from math import comb\n", "\n", "import wecopttool as wot\n", "\n", @@ -70,8 +72,11 @@ "outputs": [], "source": [ "fend = 1.875\n", - "nfreq = 150\n", - "f1 = fend / nfreq" + "nfreq_irreg = 150\n", + "f1_irreg = fend / nfreq_irreg\n", + "\n", + "f1_reg = .325/2\n", + "nfreq_reg = 12" ] }, { @@ -82,18 +87,17 @@ "source": [ "# regular\n", "amplitude = 0.15\n", - "wavefreq = 0.35\n", - "waves_regular = wot.waves.regular_wave(f1, nfreq, wavefreq, amplitude)\n", + "wavefreq = 0.325\n", + "waves_regular = wot.waves.regular_wave(f1_reg, nfreq_reg, wavefreq, amplitude)\n", "\n", "# irregular\n", "Hs = 1.5\n", "Tp = 5 \n", - "\n", "nrealizations = 2\n", "\n", "fp = 1/Tp\n", "spectrum = lambda f: wot.waves.pierson_moskowitz_spectrum(f, fp, Hs)\n", - "efth = wot.waves.omnidirectional_spectrum(f1, nfreq, spectrum, \"Pierson-Moskowitz\")\n", + "efth = wot.waves.omnidirectional_spectrum(f1_irreg, nfreq_irreg, spectrum, \"Pierson-Moskowitz\")\n", "waves_irregular = wot.waves.long_crested_wave(efth, nrealizations=nrealizations)" ] }, @@ -177,21 +181,35 @@ "outputs": [], "source": [ "buoy_props = {\n", - " 'CG': 0.242, # m\n", + " 'CG': 0.298, # m\n", " 'MOI': 7484., # kg-m^2\n", " 'Resonance frequency': 0.35, # Hz\n", "}\n", "\n", - "flywheel_props = {\n", - " 'MOI': 25, # kg-m^2\n", - " 'Coulomb friction': 2.0, # N-m\n", - " 'Viscous friction': 0.02, # N-m/rad\n", - " 'Gear ratio': 3, # -\n", + "# flywheel properties at example resonance\n", + "flywheel_properties = {\n", + " 'coulomb_friction': 4.5, # N*m\n", + " 'viscous_friction': 0.02, # N*ms/rad\n", + " 'motor_gear_ratio': 0.118,\n", + " 'MOI': 22.32, # kg*m^2\n", "}\n", "\n", - "spring_props = {\n", + "spring_properties = {\n", + " 'stiffness': 835.36, # N*m/rad\n", + " 'gear_ratio': 0.334,\n", " 'Max torque': 750, # N-m\n", " 'Max displacement': np.deg2rad(45.0), # rad\n", + "}\n", + "\n", + "# PTO properties\n", + "pto_properties = {\n", + " 'gear_ratio': 1.0,\n", + " 'torque_constant': 0.164,\n", + " 'winding_resistance': 0.0718,\n", + " 'winding_inductance': 0.0,\n", + " 'drivetrain_inertia': 0.0,\n", + " 'drivetrain_friction': 0.0,\n", + " 'drivetrain_stiffness': 0.0,\n", "}" ] }, @@ -225,7 +243,8 @@ "metadata": {}, "source": [ "#### Hydrodynamics and hydrostatics\n", - "As mentioned above, the `FloatingBody` object in Capytaine only needs to model the buoy, since no other components are being excited by the waves." + "As mentioned above, the `FloatingBody` object in Capytaine only needs to model the buoy, since no other components are being excited by the waves. \n", + "We will not use the BEM results since we are using an empirical impedance model, but the BEM problem is still solved here for comparison purposes." ] }, { @@ -239,7 +258,13 @@ "pnr_fb.center_of_mass = np.array([0., 0., buoy_props['CG']])\n", "pnr_fb.rotation_center = pnr_fb.center_of_mass\n", "ndof = pnr_fb.nb_dofs\n", - "pnr_fb.show_matplotlib()" + "pnr_fb.show_matplotlib()\n", + "\n", + "pnr_fb.inertia_matrix = xr.DataArray(data=np.asarray(([[buoy_props['MOI']]])),\n", + " dims=['influenced_dof', 'radiating_dof'],\n", + " coords={'influenced_dof': list(pnr_fb.dofs),\n", + " 'radiating_dof': list(pnr_fb.dofs)},\n", + " name=\"inertia_matrix\")" ] }, { @@ -249,13 +274,17 @@ "outputs": [], "source": [ "rho = 1025. # kg/m^3\n", - "freq = wot.frequency(f1, nfreq, False) # False -> no zero frequency\n", - "bem_data = wot.run_bem(pnr_fb, freq)\n", - "omega = bem_data.omega.values\n", + "freq_reg = wot.frequency(f1_reg, nfreq_reg, False) # False -> no zero frequency\n", + "bem_data_reg = wot.run_bem(pnr_fb, freq_reg)\n", + "omega_reg = bem_data_reg.omega.values\n", + "\n", + "freq_irreg = wot.frequency(f1_irreg, nfreq_irreg, False) # False -> no zero frequency\n", + "bem_data_irreg = wot.run_bem(pnr_fb, freq_irreg)\n", + "omega_irreg = bem_data_irreg.omega.values\n", "\n", "pnr_fb.keep_immersed_part()\n", "k_buoy = pnr_fb.compute_hydrostatic_stiffness(rho=rho).values.squeeze()\n", - "k_spring = spring_props['Max torque'] / spring_props['Max displacement']\n", + "k_spring = spring_properties['Max torque'] / spring_properties['Max displacement']\n", "print(f'Hydrostatic stiffness from Capytaine: {k_buoy} N-m/rad')\n", "print('Hydrostatic stiffness from experiment: 37204 N-m/rad')" ] @@ -264,7 +293,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "As with previous tutorials, we will plot the hydrodynamic coefficients to confirm that we are properly capturing the full excitation range of the buoy." + "Although the system has yet to be deployed as a WEC, the Coastal Pioneer Array itself has been collecting data from multiple locations. From the collected data, an empirical impedance model and excitation force spectrum has been developed. This empirical dataset can replace the BEM dataset in order to model the WEC dynamics. Here, we load in the data and convert it into two DataArrays for the intrinsic impedance and the excitation coefficients, respectively. We then interpolate from the empirical data to match the intended frequency array." ] }, { @@ -273,35 +302,68 @@ "metadata": {}, "outputs": [], "source": [ - "# Plots\n", - "fig_am, ax_am = plt.subplots(tight_layout=True, sharex=True)\n", - "fig_rd, ax_rd = plt.subplots(tight_layout=True, sharex=True)\n", - "fig_ex, ax_ex = plt.subplots(tight_layout=True, sharex=True)\n", - "\n", - "# Excitation\n", - "np.abs(bem_data.diffraction_force.sel(influenced_dof='Pitch')).plot(\n", - " ax=ax_ex, linestyle='dashed', label='Diffraction force')\n", - "np.abs(bem_data.Froude_Krylov_force.sel(influenced_dof='Pitch')).plot(\n", - " ax=ax_ex, linestyle='dashdot', label='Froude-Krylov force')\n", - "ex_handles, ex_labels = ax_ex.get_legend_handles_labels()\n", - "ax_ex.set_xlabel(f'$\\omega$', fontsize=10)\n", - "ax_ex.set_title('Wave Excitation Coefficients', fontweight='bold')\n", - "ax_ex.grid(True)\n", - "fig_ex.legend(ex_handles, ex_labels, loc='center right', frameon=False)\n", - "\n", - "# Added mass\n", - "bem_data.added_mass.sel(\n", - " radiating_dof='Pitch', influenced_dof='Pitch').plot(ax=ax_am)\n", - "ax_am.set_xlabel(f'$\\omega$', fontsize=10)\n", - "ax_am.set_title('Added Mass Coefficients', fontweight='bold')\n", - "ax_am.grid(True)\n", - "\n", - "# Radiation damping\n", - "bem_data.radiation_damping.sel(\n", - " radiating_dof='Pitch', influenced_dof='Pitch').plot(ax=ax_rd)\n", - "ax_rd.set_xlabel(f'$\\omega$', fontsize=10)\n", - "ax_rd.set_title('Radiation Damping Coefficients', fontweight='bold')\n", - "ax_rd.grid(True)" + "# load in the empirical data\n", + "datafile = 'data/pioneer_empirical_data.nc'\n", + "empirical_data = xr.load_dataset(datafile)\n", + "\n", + "exc_coeff_data = empirical_data.exc_coeff_data_real + 1j*empirical_data.exc_coeff_data_imag\n", + "Zi_data = empirical_data.Zi_data_real + 1j*empirical_data.Zi_data_imag\n", + "Zi_stiffness = empirical_data.Zi_stiffness\n", + "\n", + "# here extrapolation for impedance and padding with zeros for the excitation\n", + "exc_coeff_intrp_reg = exc_coeff_data.interp(omega = omega_reg, method='linear', kwargs={\"fill_value\": 0})\n", + "Zi_intrp_reg = Zi_data.interp(omega = omega_reg, kwargs={\"fill_value\": \"extrapolate\"})\n", + "\n", + "exc_coeff_intrp_irreg = exc_coeff_data.interp(omega = omega_irreg, method='linear', kwargs={\"fill_value\": 0})\n", + "Zi_intrp_irreg = Zi_data.interp(omega = omega_irreg, kwargs={\"fill_value\": \"extrapolate\"})" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "For validation purposes, we compare the empirical impedance and excitation to the results from BEM." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "hd = wot.add_linear_friction(bem_data_irreg, friction = None) \n", + "# we're not actually adding friction, but need the datavariables in hd \n", + "hd = wot.check_radiation_damping(hd)\n", + "\n", + "Zi_bem = wot.hydrodynamic_impedance(hd)\n", + "fig, axes = wot.utilities.plot_bode_impedance(Zi_bem,'WaveBot Intrinsic Impedance')\n", + "fig, axes = wot.utilities.plot_bode_impedance(Zi_data,'WaveBot Intrinsic Impedance',fig_axes=[fig, axes])\n", + "fig, axes = wot.utilities.plot_bode_impedance(Zi_intrp_irreg,'WaveBot Intrinsic Impedance',fig_axes=[fig, axes])\n", + "fig.set_size_inches(6, 4)\n", + "fig.legend(('BEM Data','Empirical Data','Interpolated Empirical Data'), bbox_to_anchor=(.58, .46))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "fig, ax = plt.subplots(2, 1,tight_layout=True, sharex=True, figsize=(6, 4))\n", + "\n", + "ax[0].semilogx(freq_irreg, np.real(bem_data_irreg['diffraction_force'][:,0,0]+bem_data_irreg['Froude_Krylov_force'][:,0,0]), label='BEM Data')\n", + "ax[0].semilogx(omega_data/(2*np.pi), np.real(exc_coeff_data[:,0,0]), label='Empirical Data')\n", + "ax[0].semilogx(omega_irreg/(2*np.pi), np.real(exc_coeff_intrp_irreg[:,0,0]), label='Interpolated Empirical Data')\n", + "ax[0].grid(True, which = 'both')\n", + "ax[1].semilogx(freq_irreg, np.imag(bem_data_irreg['diffraction_force'][:,0,0]+bem_data_irreg['Froude_Krylov_force'][:,0,0]), label='BEM Data')\n", + "ax[1].semilogx(omega_data/(2*np.pi), np.imag(exc_coeff_data[:,0,0]), label='Empirical Data')\n", + "ax[1].semilogx(omega_irreg/(2*np.pi), np.imag(exc_coeff_intrp_irreg[:,0,0]), label='Interpolated Empirical Data')\n", + "ax[1].grid(True, which = 'both')\n", + "\n", + "ax[0].set_ylabel('Re[Excitation]')\n", + "ax[1].set_ylabel('Im[Excitation]')\n", + "ax[1].set_xlabel('Frequency (Hz)')\n", + "ax[0].legend()" ] }, { @@ -341,8 +403,8 @@ "metadata": {}, "outputs": [], "source": [ - "nstate_pto = 2 * nfreq # PTO forces\n", - "nstate_fw = 2 * nfreq # Flywheel positions\n", + "nstate_pto = 2 * nfreq_reg # PTO forces\n", + "nstate_fw = 2 * nfreq_reg # Flywheel positions\n", "nstate_opt = nstate_pto + nstate_fw" ] }, @@ -395,10 +457,11 @@ "metadata": {}, "outputs": [], "source": [ - "def f_motor(wec, x_wec, x_opt, waves, nsubsteps=1):\n", - " motor = np.reshape(x_opt[:nstate_pto], (-1, ndof), order='F')\n", + "def force_from_generator(wec, x_wec, x_opt, waves=None, nsubsteps=1):\n", + " f_fd = np.reshape(x_opt[:nstate_pto], (-1, ndof), order='F')\n", " time_matrix = wec.time_mat_nsubsteps(nsubsteps)\n", - " return np.dot(time_matrix, motor)" + " torque = np.dot(time_matrix, f_fd) * flywheel_properties['motor_gear_ratio']\n", + " return torque" ] }, { @@ -406,8 +469,7 @@ "metadata": {}, "source": [ "##### PTO Impedance\n", - "The PTO impedance is defined using the same 2-port impedance model as in previous tutorials.\n", - "The drivetrain inertia, friction, and stiffness are not included here since they are accounted for in the modeling of the additional non-hydrodynamic degree of freedom via additional forces and constraints below." + "The PTO impedance is defined using the same 2-port impedance model as in previous tutorials. This pto model does not account for the relative velocity, generator force, etc. so it will not be used to define the PTO in the final system. Instead, it is used to create the PTO transfer matrix which is used in the calculation of electrical power." ] }, { @@ -416,57 +478,21 @@ "metadata": {}, "outputs": [], "source": [ - "torque_constant = 3.512\n", - "winding_resistance = 0.304\n", + "drivetrain_impedance = (1j*omega_reg*pto_properties['drivetrain_inertia'] +\n", + " pto_properties['drivetrain_friction'] +\n", + " 1/(1j*omega_reg)*pto_properties['drivetrain_stiffness'])\n", "\n", - "z_11 = np.array([[np.zeros(omega.shape)]])\n", - "off_diag = np.sqrt(3.0/2.0) * torque_constant\n", - "z_12 = np.array([[-1*(off_diag+0j) * np.ones(omega.shape)]])\n", - "z_21 = np.array([[-1*(off_diag+0j) * np.ones(omega.shape)]])\n", - "z_22 = np.array([[winding_resistance * np.ones(omega.shape)]])" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "When using the `PTO` module, the 2x2 impedance matrix is passed to the `wot.pto.PTO` object and converted into a transfer matrix to calculate the power at each frequency.\n", - "While we are not using the `PTO` module, we will repeat this process almost verbatim here.\n", - "See the `_make_abcd` and `_make_mimo_transfer_mat` functions in the `PTO` module if you are interested in more technical details." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "z_12_inv = np.linalg.inv(z_12.T).T\n", - "mmult = lambda a,b: np.einsum('mnr,mnr->mnr', a, b)\n", - "abcd_11 = -1 * mmult(z_12_inv, z_11)\n", - "abcd_12 = z_12_inv\n", - "abcd_21 = z_21 - mmult(z_22, mmult(z_12_inv, z_11))\n", - "abcd_22 = mmult(z_22, z_12_inv)\n", - "row_1 = np.hstack([abcd_11, abcd_12])\n", - "row_2 = np.hstack([abcd_21, abcd_22])\n", - "abcd = np.vstack([row_1, row_2])\n", - "\n", - "for idof in range(2):\n", - " for jdof in range(2):\n", - " Zp = abcd[idof, jdof, :]\n", - " re = np.real(Zp)\n", - " im = np.imag(Zp)\n", - " blocks = [np.array([[ire, iim], [iim, ire]])\n", - " for (ire, iim) in zip(re[:-1], im[:-1])]\n", - " blocks = [re[0]] + blocks + [re[-1]]\n", - " if jdof==0:\n", - " row = block_diag(*blocks)\n", - " else:\n", - " row = np.hstack([row, block_diag(*blocks)])\n", - " if idof==0:\n", - " transfer_mat = row\n", - " else:\n", - " transfer_mat = np.vstack([transfer_mat, row])" + "winding_impedance = pto_properties['winding_resistance'] + 1j*omega_reg*pto_properties['winding_inductance']\n", + "\n", + "pto_impedance_11 = -1* pto_properties['gear_ratio']**2 * drivetrain_impedance\n", + "off_diag = np.sqrt(3.0/2.0) * pto_properties['torque_constant'] * pto_properties['gear_ratio']\n", + "pto_impedance_12 = -1*(off_diag+0j) * np.ones(omega_reg.shape)\n", + "pto_impedance_21 = -1*(off_diag+0j) * np.ones(omega_reg.shape)\n", + "pto_impedance_22 = winding_impedance\n", + "pto_impedance = np.array([[pto_impedance_11, pto_impedance_12],\n", + " [pto_impedance_21, pto_impedance_22]])\n", + "\n", + "pto = wot.pto.PTO(ndof, kinematics=[1], controller=None, impedance=pto_impedance, loss=None, names=None)\n" ] }, { @@ -474,8 +500,7 @@ "metadata": {}, "source": [ "##### Power and Energy\n", - "Finally, we will wrap the relative motions, force, and impedance functions together to calculate average electrical power.\n", - "This is again very close to what is found in the `PTO` module, except `rel_velocity` replaces `wot.pto.PTO.velocity` and `f_motor` replaces `wot.pto.PTO.force`." + "Finally, we will wrap the relative motions, force, and impedance functions together to calculate average electrical power. This is again very close to what is found in the PTO module, except `rel_velocity` replaces `wot.pto.PTO.velocity` and `force_from_generator` replaces `wot.pto.PTO.force`." ] }, { @@ -485,18 +510,18 @@ "outputs": [], "source": [ "def mechanical_power(wec, x_wec, x_opt, waves, nsubsteps=1):\n", - " force_td = f_motor(wec, x_wec, x_opt, waves, nsubsteps)\n", + " force_td = force_from_generator(wec, x_wec, x_opt, waves, nsubsteps)\n", " vel_td = rel_velocity(wec, x_wec, x_opt, waves, nsubsteps)\n", " return vel_td * force_td\n", "\n", "def electrical_power(wec, x_wec, x_opt, waves, nsubsteps=1):\n", " q1_td = rel_velocity(wec, x_wec, x_opt, waves)\n", - " e1_td = f_motor(wec, x_wec, x_opt, waves)\n", + " e1_td = force_from_generator(wec, x_wec, x_opt, waves)\n", " q1 = wot.complex_to_real(wec.td_to_fd(q1_td, False))\n", " e1 = wot.complex_to_real(wec.td_to_fd(e1_td, False))\n", " vars_1 = np.hstack([q1, e1])\n", " vars_1_flat = wec.dofmat_to_vec(vars_1)\n", - " vars_2_flat = np.dot(transfer_mat, vars_1_flat)\n", + " vars_2_flat = np.dot(pto._transfer_mat, vars_1_flat)\n", " vars_2 = wot.vec_to_dofmat(vars_2_flat, 2)\n", " q2 = vars_2[:, 0]\n", " e2 = vars_2[:, 1]\n", @@ -519,10 +544,10 @@ "metadata": {}, "source": [ "### 1.4 Constraints\n", - "The Pioneer model only contains one constraint based on the motor being used:\n", + "The Pioneer model only contains one constraint based on the generator being used:\n", "\n", - " * **Peak torque** - The motor should avoid torques greater than 120 N-m.\n", - " This is the same basic constraint as `const_f_pto` from Tutorial 1, except we use our `f_motor` definition instead of `wot.pto.PTO.force_on_wec`." + "* **Peak torque** - The motor should avoid torques greater than 120 N-m. \n", + "This is the same basic constraint as `const_f_pto` from Tutorial 1, except we use our `force_from_generator` definition instead of `wot.pto.PTO.force_on_wec`." ] }, { @@ -531,10 +556,12 @@ "metadata": {}, "outputs": [], "source": [ - "torque_peak_max = 120.\n", - "def const_peak_torque_pto(wec, x_wec, x_opt, waves, nsubsteps=5):\n", - " torque = f_motor(wec, x_wec, x_opt, waves, nsubsteps)\n", - " return torque_peak_max - np.abs(torque.flatten())" + "max_generator_torque = 25.8 # N*m\n", + "nsubsteps_constraints = 5\n", + "\n", + "def constraint_max_generator_torque(wec, x_wec, x_opt, waves, nsubsteps=nsubsteps_constraints):\n", + " torque = force_from_generator(wec, x_wec, x_opt, waves, nsubsteps)\n", + " return max_generator_torque - np.abs(torque.flatten())" ] }, { @@ -544,14 +571,18 @@ "### 1.5 Additional forces\n", "Here we add in the forces acting on the bodies other than those generated from wave loading.\n", "\n", - " * **Motor damping** - The resisting torque resulting from the damping on the PTO.\n", - " This is very similar to the `wot.pto.PTO.force_on_wec` additional force given in previous tutorials. \n", - " This is already being accounted for by the `f_motor` function above.\n", - " * **Flywheel friction** - The dissipative torque from the generator and components.\n", - " This is defined as a nonlinear force using both Coulomb friction (dependent on the PTO direction) and the viscous friction (dependent on the PTO speed).\n", - " * **Magnetic spring** - The restoring torque caused by the torsional spring between the buoy and flywheel.\n", - " Note the gear ratio is included in this equation, as the gearbox is connected in series with the spring.\n", - " The position of the spring is the relative position of the flywheel and buoy scaled by the reciprocal of gear ratio squared. " + "* **Flywheel friction** - The dissipative torque from the generator and components. This is defined as a nonlinear force using both Coulomb friction (dependent on the PTO direction) and the viscous friction (dependent on the PTO speed).\n", + "\n", + "* **Magnetic spring** - The restoring torque caused by the torsional spring between the buoy and flywheel. Note a spring gear ratio is included in this equation, as a gearbox is connected in series with the spring. The position of the spring is the relative position of the flywheel and buoy scaled by the reciprocal of gear ratio squared. There are 2 options here: \n", + "\n", + " * Linear spring: \n", + " * Used for simplified modeling purposes \n", + " * The WecOptTool optimization problem is first solved with the linear spring, the results from which are used as initialization for the nonlinear spring case\n", + "\n", + " * Nonlinear spring: \n", + " * Represents the real spring design developed for this application \n", + " * The resultant torque has a periodic \"tilted sine\" profile with respect to the spring angle. This design emulates a linear spring until about 135 degrees in either direction before repreating the torque profile.\n", + " * Final result uses the nonlinear spring" ] }, { @@ -560,17 +591,46 @@ "metadata": {}, "outputs": [], "source": [ - "def f_friction(wec, x_wec, x_opt, waves, nsubsteps=1):\n", - " rel_vel = rel_velocity(wec, x_wec, x_opt, waves, nsubsteps)\n", - " return -1 * (\n", - " np.tanh(rel_vel) * flywheel_props['Coulomb friction']\n", - " + rel_vel * flywheel_props['Viscous friction']\n", - " )\n", + "def force_from_friction(wec, x_wec, x_opt, waves = None, nsubsteps = 1):\n", + " rel_vel = rel_velocity(wec, x_wec, x_opt, waves, nsubsteps) * flywheel_properties['motor_gear_ratio']\n", + " fric = -1*(\n", + " np.tanh(rel_vel)*flywheel_properties['coulomb_friction'] +\n", + " rel_vel*flywheel_properties['viscous_friction']\n", + " ) * flywheel_properties['motor_gear_ratio']\n", + " return fric\n", + "\n", + "def linear_spring(pos):\n", + " return spring_properties['gear_ratio'] * -spring_properties['stiffness'] * pos\n", + "\n", + "def force_from_lin_spring(wec, x_wec, x_opt, waves = None, nsubsteps = 1):\n", + " pos = rel_position(wec, x_wec, x_opt, waves, nsubsteps) * spring_properties['gear_ratio']\n", + " return linear_spring(pos)\n", + "\n", + "def nonlinear_spring(pos):\n", + " # 135 deg nonlinear spring\n", + " spring_eq_pos_td = pos - np.pi\n", + " n = 12\n", + " slope = 1/(2**(2*n))*comb(2*n,n)\n", + " scale = 1/slope\n", + " new_pos = 0\n", + " for ind in range(n):\n", + " k = ind+1\n", + " coeffs = comb(2*n, n-k)/(k*(2**(2*n-1)))\n", + " new_pos = new_pos - coeffs*np.sin(k*spring_eq_pos_td)\n", + " return spring_properties['gear_ratio'] * -spring_properties['stiffness'] * scale * new_pos\n", + "\n", + "def force_from_nl_spring(wec, x_wec, x_opt, waves = None, nsubsteps = 1):\n", + " pos = rel_position(wec, x_wec, x_opt, waves, nsubsteps) * spring_properties['gear_ratio']\n", + " return nonlinear_spring(pos)\n", + "\n", + "spring_angle_rad = np.arange(-2*np.pi, 2*np.pi, np.pi/200)\n", "\n", - "def f_spring(wec, x_wec, x_opt, waves, nsubsteps=1):\n", - " pos = rel_position(wec, x_wec, x_opt, waves, nsubsteps) / (flywheel_props['Gear ratio']**2)\n", - " spring_force = -k_spring * pos\n", - " return spring_force" + "plt.figure()\n", + "plt.plot(spring_angle_rad,linear_spring(spring_angle_rad), label = 'Linear spring')\n", + "plt.plot(spring_angle_rad,nonlinear_spring(spring_angle_rad), label = 'Nonlinear spring',ls='--')\n", + "plt.xlabel('Spring displacement (rad)')\n", + "plt.ylabel('Spring torque [N-m]')\n", + "plt.legend()" ] }, { @@ -582,6 +642,9 @@ "\n", "$$ r(x) = I \\alpha - \\tau = 0 $$\n", "\n", + "Note that there are 2 residual equations below, one using the linear spring and another using the nonlinear spring. \n", + "Both are included because we solve the problem using the linear spring to given initial conditions for the nonlinear spring solve. \n", + "More information is included in Section 2.1.\n", "This is the same structure as the residual for the WEC dynamics described in the [WecOptTool theory documentation](https://sandialabs.github.io/WecOptTool/theory.html).\n", "Also compare this equation to the 2nd dynamics equation listed [above](#pitch-resonator).\n", "\n", @@ -595,27 +658,37 @@ "metadata": {}, "outputs": [], "source": [ - "def fw_inertia(wec, x_wec, x_opt, waves, nsubsteps=1):\n", + "def flywheel_inertia(wec, x_wec, x_opt, waves = None, nsubsteps = 1):\n", " pos_fw = wec.vec_to_dofmat(x_opt[nstate_pto:])\n", " acc_fw = np.dot(wec.derivative2_mat, pos_fw)\n", " time_matrix = wec.time_mat_nsubsteps(nsubsteps)\n", " acc_fw = np.dot(time_matrix, acc_fw)\n", - " return flywheel_props['MOI'] * acc_fw\n", - "\n", - "def const_flywheel_resid(wec, x_wec, x_opt, waves):\n", - " return (fw_inertia(wec, x_wec, x_opt, waves)\n", - " + f_spring(wec, x_wec, x_opt, waves)\n", - " + f_friction(wec, x_wec, x_opt, waves)\n", - " + f_motor(wec, x_wec, x_opt, waves)\n", - " ).flatten()" + " return flywheel_properties['MOI'] * acc_fw\n", + "\n", + "def flywheel_residual_lin_spring(wec, x_wec, x_opt, waves = None, nsubsteps = 1):\n", + " resid = (\n", + " flywheel_inertia(wec, x_wec, x_opt, waves, nsubsteps) +\n", + " force_from_lin_spring(wec, x_wec, x_opt, waves, nsubsteps) +\n", + " force_from_friction(wec, x_wec, x_opt, waves, nsubsteps) +\n", + " force_from_generator(wec, x_wec, x_opt, waves, nsubsteps)\n", + " )\n", + " return resid.flatten()\n", + "\n", + "def flywheel_residual_nl_spring(wec, x_wec, x_opt, waves = None, nsubsteps = 1):\n", + " resid = (\n", + " flywheel_inertia(wec, x_wec, x_opt, waves, nsubsteps) +\n", + " force_from_nl_spring(wec, x_wec, x_opt, waves, nsubsteps) +\n", + " force_from_friction(wec, x_wec, x_opt, waves, nsubsteps) +\n", + " force_from_generator(wec, x_wec, x_opt, waves, nsubsteps)\n", + " )\n", + " return resid.flatten()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We now create the additional forces and constaints that will be passed to the `WEC` object.\n", - "Note that `f_add` only pertains to the buoy since that is the only degree of freedom included in the BEM data; our flywheel residual equation handles these forces for the flywheel in the opposite direction." + "We now create the additional forces and constaints that will be passed to the `WEC` object. Note that `f_add` only pertains to the buoy since that is the only degree of freedom included in the BEM data; our flywheel residual equation handles these forces for the flywheel in the opposite direction. Just like for the flywheel residual equations, there are 2 different sets of added forces and constraints, one including the linear spring and another including the nonlinear spring." ] }, { @@ -624,15 +697,26 @@ "metadata": {}, "outputs": [], "source": [ - "f_add = {\n", - " 'Motor': f_motor,\n", - " 'Friction': f_friction,\n", - " 'Spring': f_spring, \n", + "f_add_lin_spring = {\n", + " 'Generator': force_from_generator,\n", + " 'Friction': force_from_friction,\n", + " 'Spring': force_from_lin_spring,\n", + "}\n", + "\n", + "f_add_nl_spring = {\n", + " 'Generator': force_from_generator,\n", + " 'Friction': force_from_friction,\n", + " 'Spring': force_from_nl_spring,\n", "}\n", "\n", - "constraints = [\n", - " {'type': 'ineq', 'fun': const_peak_torque_pto},\n", - " {'type': 'eq', 'fun': const_flywheel_resid},\n", + "constraints_lin_spring = [\n", + " {'type': 'eq', 'fun': flywheel_residual_lin_spring}, # flywheel EoM\n", + " {'type': 'ineq', 'fun': constraint_max_generator_torque},\n", + "]\n", + "\n", + "constraints_nl_spring = [\n", + " {'type': 'eq', 'fun': flywheel_residual_nl_spring}, # flywheel EoM\n", + " {'type': 'ineq', 'fun': constraint_max_generator_torque},\n", "]" ] }, @@ -641,7 +725,7 @@ "metadata": {}, "source": [ "### 1.7 WEC object\n", - "We now have all the information required to create our `WEC` object." + "We now have all the information required to create our `WEC` object. The `from_impedance` method is used, because we are using our empirical impedance data. We create one `WEC` object for each of the linear and nonlinear springs." ] }, { @@ -650,11 +734,15 @@ "metadata": {}, "outputs": [], "source": [ - "wec = wot.WEC.from_bem(bem_data,\n", - " f_add=f_add,\n", - " constraints=constraints,\n", - " uniform_shift=False,\n", - " dof_names=bem_data.influenced_dof.values,)" + "wec_lin = wot.WEC.from_impedance(freq_reg, Zi_intrp_reg,exc_coeff_intrp_reg,\n", + " Zi_stiffness,\n", + " f_add_lin_spring,\n", + " constraints_lin_spring)\n", + "\n", + "wec_nl = wot.WEC.from_impedance(freq_reg, Zi_intrp_reg,exc_coeff_intrp_reg,\n", + " Zi_stiffness,\n", + " f_add_nl_spring,\n", + " constraints_nl_spring)" ] }, { @@ -664,7 +752,7 @@ "## 2. Regular wave results\n", "\n", "### 2.1 Solve\n", - "As in previous tutorials, we will optimize for electrical power absorption." + "As in previous tutorials, we will optimize for electrical power absorption. First, we solve the optimization problem using the linear spring case, then we use those results to initialize the nonlinear spring case. This process helps the optimizer (which can have difficulty with highly nonlinear force profiles) converge quicker and more consistently." ] }, { @@ -674,12 +762,24 @@ "outputs": [], "source": [ "obj_fun = average_electrical_power\n", - "results = wec.solve(\n", + "results = wec_lin.solve(\n", " waves_regular,\n", " obj_fun,\n", " nstate_opt,\n", - " scale_x_wec=1e1,\n", - " scale_x_opt=1e-2,\n", + " scale_x_wec=1e0,\n", + " scale_x_opt=np.concatenate((np.array([1e-1])*np.ones(nstate_pto), 1 * np.ones(nstate_fw))),\n", + " scale_obj=1e-2,\n", + ")\n", + "x_wec_0, x_opt_0 = wec_lin.decompose_state(results[0].x)\n", + "\n", + "results = wec_nl.solve(\n", + " waves_regular,\n", + " obj_fun,\n", + " nstate_opt,\n", + " x_wec_0=x_wec_0, # initialize with result from linear spring case\n", + " x_opt_0=x_opt_0, # initialize with result from linear spring case\n", + " scale_x_wec=1e0,\n", + " scale_x_opt=np.concatenate((np.array([1e-1])*np.ones(nstate_pto), 1 * np.ones(nstate_fw))),\n", " scale_obj=1e-2,\n", ")\n", "print(f'Optimal average power: {results[0].fun:.2f} W')" @@ -690,7 +790,7 @@ "metadata": {}, "source": [ "### 2.2 Post-process and plot\n", - "Again, since we are not using the `PTO` module, post-processing using `wot.pto.PTO.post_process` is not an option here, so we have to manually post-process the outputs related to the PTO and flywheel.\n", + "Again, since we are not actually using the `PTO` module in the optimization problem, post-processing using `wot.pto.PTO.post_process` is not an option here, so we have to manually post-process the outputs related to the PTO and flywheel.\n", "This is pretty intuitive using the functions we created earlier.\n", "The outputs related to the buoy can still be derived directly from `wot.wec.post_process`." ] @@ -702,19 +802,20 @@ "outputs": [], "source": [ "nsubsteps = 5\n", - "wec_fdom, wec_tdom = wec.post_process(wec, results, waves_regular, nsubsteps=nsubsteps)\n", + "wec_fdom, wec_tdom = wec_nl.post_process(wec_nl, results, waves_regular, nsubsteps=nsubsteps)\n", "\n", "# Manually post-process PTO and flywheel outputs\n", - "x_wec, x_opt = wot.decompose_state(results[0].x, 1, nfreq)\n", - "fw_pos = np.dot(wec.time_mat_nsubsteps(nsubsteps), x_opt[nstate_pto:])\n", - "pto_pos = rel_position(wec, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", - "pto_vel = rel_velocity(wec, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", - "pto_force = f_motor(wec, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", - "pto_force_fd = wec.td_to_fd(pto_force[::nsubsteps])\n", - "pto_mech_power = mechanical_power(wec, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", - "pto_elec_power = electrical_power(wec, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", + "x_wec, x_opt = wot.decompose_state(results[0].x, 1, nfreq_reg)\n", + "fw_pos = np.dot(wec_nl.time_mat_nsubsteps(nsubsteps), x_opt[nstate_pto:])\n", + "pto_pos = rel_position(wec_nl, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", + "pto_vel = rel_velocity(wec_nl, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", + "pto_force = force_from_generator(wec_nl, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", + "pto_force_fd = wec_nl.td_to_fd(pto_force[::nsubsteps])\n", + "spring_force = force_from_nl_spring(wec_nl, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", + "pto_mech_power = mechanical_power(wec_nl, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", + "pto_elec_power = electrical_power(wec_nl, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", "avg_mech_power = np.mean(pto_mech_power)\n", - "avg_elec_power = np.mean(pto_elec_power)\n" + "avg_elec_power = np.mean(pto_elec_power)" ] }, { @@ -727,6 +828,7 @@ " * The gearing in series with the torsional spring (which reduces the effective spring stiffness) significantly amplifies the rotation of the flywheel despite the buoy only pitching modestly.\n", " The gear ratio of 3 was selected by the Pioneer team to keep the system in resonance considering the spring stiffness, moment of inertia of the flywheel, and the resonance frequency of the buoy.\n", " Try increasing the gear ratio or flywheel moment of inertia—this will actually reduce the power generated since it will bring the effective stiffness of the flywheel out of resonance, causing less motion relative to the buoy.\n", + " * The nonlinear spring torque is sinusoidal because it stays within the linear region of operation.\n", " * The mechanical and electrical power outputs are similar, since our impedance model has only a small amount of resistance and no inductance.\n", " * The buoy's pitch amplitude is larger than expected for this device and is likely due to underestimation of the radiation damping by the BEM. " ] @@ -737,7 +839,7 @@ "metadata": {}, "outputs": [], "source": [ - "fig, ax = plt.subplots(nrows=4, sharex=True, figsize=(12, 12))\n", + "fig, ax = plt.subplots(nrows=5, sharex=True, figsize=(12, 12))\n", "t = wec_tdom[0].time.values\n", "\n", "# Positions\n", @@ -755,14 +857,15 @@ "\n", "# Torques\n", "ax[1].plot(t, pto_force, c='k')\n", - "ax[1].plot([t[0], t[-1]], [torque_peak_max, torque_peak_max], 'k--')\n", - "ax[1].plot([t[0], t[-1]], [-1*torque_peak_max, -1*torque_peak_max], 'k--')\n", + "ax[1].plot([t[0], t[-1]], [max_generator_torque, max_generator_torque], 'k--')\n", + "ax[1].plot([t[0], t[-1]], [-1*max_generator_torque, -1*max_generator_torque], 'k--')\n", "ax[1].set_ylabel('PTO torque [N-m]')\n", "ax[1].set_title('Torque')\n", "\n", "ax1 = ax[1].twinx()\n", "ax1.tick_params(axis='y', labelcolor='b')\n", - "wec_tdom[0]['force'].sel(type=['Froude_Krylov', 'diffraction']).sum('type').plot(ax=ax1, c='b')\n", + "# when using an impedance model, the excitation force cannot be split up into Froude-Krylov and diffraction\n", + "wec_tdom[0]['force'].sel(type='excitation').plot(ax=ax1, c='b') \n", "ax1.set_ylabel('Excitation torque [N-m]', color='b')\n", "ax1.set_title('Torque')\n", "\n", @@ -771,13 +874,24 @@ "ax[2].set_title('PTO Velocity')\n", "ax[2].set_ylabel('Velocity [deg/s]')\n", "\n", + "# Spring\n", + "ax[3].plot(t, spring_force, c='k')\n", + "ax[3].set_title('Nonlinear Spring Torque')\n", + "ax[3].set_ylabel('Torque [N-m]')\n", + "\n", + "ax3 = ax[3].twinx()\n", + "ax3.tick_params(axis='y', labelcolor='b')\n", + "ax3.plot(t, pto_pos, c='b')\n", + "ax3.set_ylabel('PTO pos. [deg]', color='b')\n", + "ax3.set_title('')\n", + "\n", "# Power\n", - "ax[3].plot(t, pto_mech_power, label='Mech. power ($\\\\bar{P}_{mech}$: ' + f'{avg_mech_power:.1f} W)')\n", - "ax[3].plot(t, pto_elec_power, linestyle='dashdot', label='Elec. power ($\\\\bar{P}_{elec}$: ' + f'{avg_elec_power:.1f} W)')\n", - "ax[3].set_title('Power Output')\n", - "ax[3].set_ylabel('Power [W]')\n", - "ax[3].legend()\n", - "ax[3].set_xlabel('Time [s]')\n", + "ax[4].plot(t, pto_mech_power, label='Mech. power ($\\\\bar{P}_{mech}$: ' + f'{avg_mech_power:.1f} W)')\n", + "ax[4].plot(t, pto_elec_power, linestyle='dashdot', label='Elec. power ($\\\\bar{P}_{elec}$: ' + f'{avg_elec_power:.1f} W)')\n", + "ax[4].set_title('Power Output')\n", + "ax[4].set_ylabel('Power [W]')\n", + "ax[4].legend()\n", + "ax[4].set_xlabel('Time [s]')\n", "\n", "for axi in ax:\n", " axi.grid()\n", @@ -804,13 +918,10 @@ "metadata": {}, "outputs": [], "source": [ - "hydro_data = wot.add_linear_friction(bem_data, friction=None) \n", - "hydro_data = wot.check_radiation_damping(hydro_data, uniform_shift=False) \n", - "\n", - "Zi = wot.hydrodynamic_impedance(hydro_data)\n", + "Zi = Zi_intrp_reg\n", "Rad_res = np.real(Zi.squeeze())\n", "\n", - "Fex = wec_fdom[0].force.sel(type=['Froude_Krylov', 'diffraction']).sum('type')\n", + "Fex = wec_fdom[0].force.sel(type=['excitation'])\n", "Vel = wec_fdom[0].vel\n", "\n", "P_max_absorbable = (np.abs(Fex)**2/(8*Rad_res) ).squeeze().sum('omega').item() # after Falnes Eq. 6.10\n", @@ -823,16 +934,14 @@ " 'Actual Excitation': -1*(np.real(P_excited)), \n", "}\n", "power_flows['Unused Potential'] = power_flows['Optimal Excitation'] - power_flows['Actual Excitation']\n", - "power_flows['Absorbed'] = power_flows['Actual Excitation'] - power_flows['Radiated'] # after Falnes Eq. 6.2 \n", - "#absorbed by WEC-PTO (difference between actual excitation power and actual radiated power needs to be absorbed by PTO)" + "power_flows['Absorbed'] = power_flows['Actual Excitation'] - power_flows['Radiated'] # after Falnes Eq. 6.2 " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "We also calculate the power dissipated due to flywheel friction and make sure that the absorbed power (calculated as the difference between excited and radiated power) matches the sum of mechanical power captured by the PTO and the power dissipated due to flywheel friction.\n", - "We use a relative tolerance of 1%." + "We also calculate the power dissipated due to flywheel friction and make sure that the absorbed power (calculated as the difference between excited and radiated power) matches the sum of mechanical power captured by the PTO and the power dissipated due to flywheel friction. We use a relative tolerance of 1%." ] }, { @@ -841,6 +950,7 @@ "metadata": {}, "outputs": [], "source": [ + "\n", "def fw_velocity(wec, x_wec, x_opt, waves, nsubsteps=1):\n", " pos_fw = wec.vec_to_dofmat(x_opt[nstate_pto:])\n", " vel_fw = np.dot(wec.derivative_mat, pos_fw)\n", @@ -848,12 +958,12 @@ " return np.dot(time_matrix, vel_fw)\n", "\n", "def fw_friction_power(wec, x_wec, x_opt, waves, nsubsteps=1):\n", - " force_td = f_friction(wec, x_wec, x_opt, waves, nsubsteps)\n", + " force_td = force_from_friction(wec, x_wec, x_opt, waves, nsubsteps)\n", " vel_td = fw_velocity(wec, x_wec, x_opt, waves, nsubsteps)\n", " return vel_td * force_td\n", "\n", - "fw_fric_power = fw_friction_power(wec, x_wec, x_opt, waves_regular, nsubsteps)\n", - "avg_fw_fric_power = np.mean(fw_fric_power)\n" + "fw_fric_power = fw_friction_power(wec_nl, x_wec, x_opt, waves_regular, nsubsteps)\n", + "avg_fw_fric_power = np.mean(fw_fric_power)" ] }, { @@ -862,8 +972,8 @@ "metadata": {}, "outputs": [], "source": [ - "power_flows['Electrical (solver)']= avg_elec_power #solver determines sign\n", - "power_flows['Mechanical (solver)']= avg_mech_power - avg_fw_fric_power #solver determines sign, friction is dissipated\n", + "power_flows['Electrical (solver)'] = avg_elec_power #solver determines sign\n", + "power_flows['Mechanical (solver)'] = avg_mech_power - avg_fw_fric_power #solver determines sign, friction is dissipated\n", "power_flows['PTO Loss'] = power_flows['Mechanical (solver)'] - power_flows['Electrical (solver)'] " ] }, @@ -873,7 +983,7 @@ "metadata": {}, "outputs": [], "source": [ - "wot.utilities.plot_power_flow(power_flows)" + "wot.utilities.plot_power_flow(power_flows,tolerance=-1e-2*power_flows['Optimal Excitation']) " ] }, { @@ -883,7 +993,7 @@ "## 3. Irregular wave\n", "\n", "### 3.1 Solve\n", - "We will now run the same analysis for irregular waves. \n", + "We will now run the same analysis for irregular waves. First, we need to recreate the PTO and WEC with the irregular frequency spectrum.\n", "\n", "Interestingly, due to the narrow banded resonance of the flywheel, the controller attempts to make the excitation force monochromatic with the resonant frequency.\n", "To achieve this, it uses significant reactive power (power by the PTO into the system).\n", @@ -899,17 +1009,72 @@ "metadata": {}, "outputs": [], "source": [ + "nstate_pto = 2 * nfreq_irreg # PTO forces\n", + "nstate_fw = 2 * nfreq_irreg # Flywheel positions\n", + "nstate_opt = nstate_pto + nstate_fw\n", + "\n", + "drivetrain_impedance = (1j*omega_irreg*pto_properties['drivetrain_inertia'] +\n", + " pto_properties['drivetrain_friction'] +\n", + " 1/(1j*omega_irreg)*pto_properties['drivetrain_stiffness'])\n", + "\n", + "winding_impedance = pto_properties['winding_resistance'] + 1j*omega_irreg*pto_properties['winding_inductance']\n", + "\n", + "pto_impedance_11 = -1* pto_properties['gear_ratio']**2 * drivetrain_impedance\n", + "off_diag = np.sqrt(3.0/2.0) * pto_properties['torque_constant'] * pto_properties['gear_ratio']\n", + "pto_impedance_12 = -1*(off_diag+0j) * np.ones(omega_irreg.shape)\n", + "pto_impedance_21 = -1*(off_diag+0j) * np.ones(omega_irreg.shape)\n", + "pto_impedance_22 = winding_impedance\n", + "pto_impedance = np.array([[pto_impedance_11, pto_impedance_12],\n", + " [pto_impedance_21, pto_impedance_22]])\n", + "\n", + "pto = wot.pto.PTO(ndof, kinematics=[1], controller=None, impedance=pto_impedance, loss=None, names=None)\n", + "\n", + "wec_lin = wot.WEC.from_impedance(freq_irreg, \n", + " Zi_intrp_irreg,\n", + " exc_coeff_intrp_irreg,\n", + " Zi_stiffness,\n", + " f_add_lin_spring,\n", + " constraints_lin_spring)\n", + "\n", + "wec_nl = wot.WEC.from_impedance(freq_irreg, \n", + " Zi_intrp_irreg,\n", + " exc_coeff_intrp_irreg,\n", + " Zi_stiffness,\n", + " f_add_nl_spring,\n", + " constraints_nl_spring)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import logging \n", + "logging.getLogger().setLevel(logging.DEBUG)\n", + "\n", "obj_fun = average_electrical_power\n", - "results = wec.solve(\n", + "results = wec_lin.solve(\n", " waves_irregular,\n", " obj_fun,\n", " nstate_opt,\n", - " scale_x_wec=1e1,\n", - " scale_x_opt=1e-2,\n", + " scale_x_wec=1e0,\n", + " scale_x_opt=np.concatenate((np.array([1e-1])*np.ones(nstate_pto), 1 * np.ones(nstate_fw))),\n", " scale_obj=1e-2,\n", ")\n", - "power_results = [result.fun for result in results]\n", - "print(f'Optimal average power: {np.mean(power_results):.2f} W')" + "x_wec_0, x_opt_0 = wec_lin.decompose_state(results[0].x)\n", + "\n", + "results = wec_nl.solve(\n", + " waves_irregular,\n", + " obj_fun,\n", + " nstate_opt,\n", + " x_wec_0=x_wec_0, # initialize with result from linear spring case\n", + " x_opt_0=x_opt_0, # initialize with result from linear spring case\n", + " scale_x_wec=1e0,\n", + " scale_x_opt=np.concatenate((np.array([1e-1])*np.ones(nstate_pto), 1 * np.ones(nstate_fw))),\n", + " scale_obj=1e-2,\n", + ")\n", + "print(f'Optimal average power: {results[0].fun:.2f} W')" ] }, { @@ -926,17 +1091,18 @@ "outputs": [], "source": [ "nsubsteps = 5\n", - "wec_fdom, wec_tdom = wec.post_process(wec,results, waves_irregular, nsubsteps=nsubsteps)\n", + "wec_fdom, wec_tdom = wec_nl.post_process(wec_nl,results, waves_irregular, nsubsteps=nsubsteps)\n", "\n", "# Manually post-process PTO and flywheel outputs\n", - "x_wec, x_opt = wot.decompose_state(results[0].x, 1, nfreq)\n", - "fw_pos = np.dot(wec.time_mat_nsubsteps(nsubsteps), x_opt[nstate_pto:])\n", - "pto_pos = rel_position(wec, x_wec, x_opt, waves_irregular.sel(realization=0), nsubsteps)\n", - "pto_vel = rel_velocity(wec, x_wec, x_opt, waves_irregular.sel(realization=0), nsubsteps)\n", - "pto_force = f_motor(wec, x_wec, x_opt, waves_irregular.sel(realization=0), nsubsteps)\n", - "pto_force_fd = wec.td_to_fd(pto_force[::nsubsteps])\n", - "pto_mech_power = mechanical_power(wec, x_wec, x_opt, waves_irregular.sel(realization=0), nsubsteps)\n", - "pto_elec_power = electrical_power(wec, x_wec, x_opt, waves_irregular.sel(realization=0), nsubsteps)\n", + "x_wec, x_opt = wot.decompose_state(results[0].x, 1, nfreq_irreg)\n", + "fw_pos = np.dot(wec_nl.time_mat_nsubsteps(nsubsteps), x_opt[nstate_pto:])\n", + "pto_pos = rel_position(wec_nl, x_wec, x_opt, waves_irregular.sel(realization=0), nsubsteps)\n", + "pto_vel = rel_velocity(wec_nl, x_wec, x_opt, waves_irregular.sel(realization=0), nsubsteps)\n", + "pto_force = force_from_generator(wec_nl, x_wec, x_opt, waves_irregular.sel(realization=0), nsubsteps)\n", + "pto_force_fd = wec_nl.td_to_fd(pto_force[::nsubsteps])\n", + "spring_force = force_from_nl_spring(wec_nl, x_wec, x_opt, waves_regular.sel(realization=0), nsubsteps)\n", + "pto_mech_power = mechanical_power(wec_nl, x_wec, x_opt, waves_irregular.sel(realization=0), nsubsteps)\n", + "pto_elec_power = electrical_power(wec_nl, x_wec, x_opt, waves_irregular.sel(realization=0), nsubsteps)\n", "avg_mech_power = np.mean(pto_mech_power)\n", "avg_elec_power = np.mean(pto_elec_power)" ] @@ -947,7 +1113,7 @@ "metadata": {}, "outputs": [], "source": [ - "fig, ax = plt.subplots(nrows=4, sharex=True, figsize=(12, 12))\n", + "fig, ax = plt.subplots(nrows=5, sharex=True, figsize=(12, 12))\n", "t = wec_tdom[0].time.values\n", "\n", "# Positions\n", @@ -965,14 +1131,14 @@ "\n", "# Torques\n", "ax[1].plot(t, pto_force, c='k')\n", - "ax[1].plot([t[0], t[-1]], [torque_peak_max, torque_peak_max], 'k--')\n", - "ax[1].plot([t[0], t[-1]], [-1*torque_peak_max, -1*torque_peak_max], 'k--')\n", + "ax[1].plot([t[0], t[-1]], [max_generator_torque, max_generator_torque], 'k--')\n", + "ax[1].plot([t[0], t[-1]], [-1*max_generator_torque, -1*max_generator_torque], 'k--')\n", "ax[1].set_ylabel('PTO torque [N-m]')\n", "ax[1].set_title('Torque')\n", "\n", "ax1 = ax[1].twinx()\n", "ax1.tick_params(axis='y', labelcolor='b')\n", - "wec_tdom[0]['force'].sel(type=['Froude_Krylov', 'diffraction']).sum('type').plot(ax=ax1, c='b')\n", + "wec_tdom[0]['force'].sel(type=['excitation']).plot(ax=ax1, c='b')\n", "ax1.set_ylabel('Excitation torque [N-m]', color='b')\n", "ax1.set_title('Torque')\n", "\n", @@ -981,13 +1147,24 @@ "ax[2].set_title('PTO Velocity')\n", "ax[2].set_ylabel('Velocity [deg/s]')\n", "\n", + "# Spring\n", + "ax[3].plot(t, spring_force, c='k')\n", + "ax[3].set_title('Nonlinear Spring Torque')\n", + "ax[3].set_ylabel('Torque [N-m]')\n", + "\n", + "ax3 = ax[3].twinx()\n", + "ax3.tick_params(axis='y', labelcolor='b')\n", + "ax3.plot(t, pto_pos, c='b')\n", + "ax3.set_ylabel('PTO pos. [deg]', color='b')\n", + "ax3.set_title('')\n", + "\n", "# Power\n", - "ax[3].plot(t, pto_mech_power, label='Mech. power ($\\\\bar{P}_{mech}$: ' + f'{avg_mech_power:.1f} W)')\n", - "ax[3].plot(t, pto_elec_power, linestyle='dashdot', label='Elec. power ($\\\\bar{P}_{elec}$: ' + f'{avg_elec_power:.1f} W)')\n", - "ax[3].set_title('Power Output')\n", - "ax[3].set_ylabel('Power [W]')\n", - "ax[3].legend()\n", - "ax[3].set_xlabel('Time [s]')\n", + "ax[4].plot(t, pto_mech_power, label='Mech. power ($\\\\bar{P}_{mech}$: ' + f'{avg_mech_power:.1f} W)')\n", + "ax[4].plot(t, pto_elec_power, linestyle='dashdot', label='Elec. power ($\\\\bar{P}_{elec}$: ' + f'{avg_elec_power:.1f} W)')\n", + "ax[4].set_title('Power Output')\n", + "ax[4].set_ylabel('Power [W]')\n", + "ax[4].legend()\n", + "ax[4].set_xlabel('Time [s]')\n", "\n", "for axi in ax:\n", " axi.grid()\n", diff --git a/wecopttool/utilities.py b/wecopttool/utilities.py index b2192dff6..32d25e24a 100644 --- a/wecopttool/utilities.py +++ b/wecopttool/utilities.py @@ -134,6 +134,7 @@ def plot_hydrodynamic_coefficients(bem_data, def plot_bode_impedance(impedance: DataArray, title: Optional[str]= '', + fig_axes: Optional[list(Figure, Axes)] = None, #plot_natural_freq: Optional[bool] = False, )-> tuple(Figure, Axes): """Plot Bode graph from wecoptool impedance data array. @@ -152,14 +153,18 @@ def plot_bode_impedance(impedance: DataArray, mag = 20.0 * np.log10(np.abs(impedance)) phase = np.rad2deg(np.unwrap(np.angle(impedance))) freq = impedance.omega.values/2/np.pi - fig, axes = plt.subplots( - 2*len(radiating_dofs), - len(influenced_dofs), - tight_layout=True, - sharex=True, - figsize=(3*len(radiating_dofs), 3*len(influenced_dofs)), - squeeze=False - ) + if fig_axes is None: + fig, axes = plt.subplots( + 2*len(radiating_dofs), + len(influenced_dofs), + tight_layout=True, + sharex=True, + figsize=(3*len(radiating_dofs), 3*len(influenced_dofs)), + squeeze=False + ) + else: + fig = fig_axes[0] + axes = fig_axes[1] fig.suptitle(title + ' Bode Plots', fontweight='bold') sp_idx = 0 @@ -270,7 +275,9 @@ def calculate_power_flows(wec, return power_flows -def plot_power_flow(power_flows: dict[str, float])-> tuple(Figure, Axes): +def plot_power_flow(power_flows: dict[str, float], + tolerance: Optional[float] = None, +)-> tuple(Figure, Axes): """Plot power flow through a WEC as Sankey diagram. Parameters @@ -278,10 +285,15 @@ def plot_power_flow(power_flows: dict[str, float])-> tuple(Figure, Axes): power_flows Power flow dictionary produced by for example by :py:func:`wecopttool.utilities.calculate_power_flows`. - Required keys: 'Optimal Excitation', 'Radiated', 'Actual Excitation' - 'Electrical (solver)', 'Mechanical (solver)', - 'Absorbed', 'Unused Potential', 'PTO Loss' + Required keys: 'Optimal Excitation', 'Radiated', 'Actual Excitation', + 'Electrical (solver)', 'Mechanical (solver)', + 'Absorbed', 'Unused Potential', 'PTO Loss' + tolerance + Tolerance value for sankey diagram. """ + if tolerance is None: + tolerance = -1e-03*power_flows['Optimal Excitation'] + # fig = plt.figure(figsize = [8,4]) # ax = fig.add_subplot(1, 1, 1,) fig, ax = plt.subplots(1, 1, @@ -295,7 +307,7 @@ def plot_power_flow(power_flows: dict[str, float])-> tuple(Figure, Axes): offset= 0, format = '%.1f', shoulder = 0.02, - tolerance=-1e-03*power_flows['Optimal Excitation'], + tolerance = tolerance, unit = 'W' ) From fa0b584372a963bfeb7af2685c4e60d78dac1718 Mon Sep 17 00:00:00 2001 From: jtgrasb <87095491+jtgrasb@users.noreply.github.com> Date: Tue, 8 Oct 2024 07:57:41 -0600 Subject: [PATCH 20/27] Fix PTO force attribute and tutorial 1 constraint (#369) --- examples/tutorial_1_WaveBot.ipynb | 4 ++-- wecopttool/pto.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/tutorial_1_WaveBot.ipynb b/examples/tutorial_1_WaveBot.ipynb index 00d2badb6..a4d4dfb24 100644 --- a/examples/tutorial_1_WaveBot.ipynb +++ b/examples/tutorial_1_WaveBot.ipynb @@ -359,7 +359,7 @@ "nsubsteps = 4\n", "\n", "def const_f_pto(wec, x_wec, x_opt, waves): # Format for scipy.optimize.minimize\n", - " f = pto.force_on_wec(wec, x_wec, x_opt, waves, nsubsteps)\n", + " f = pto.force(wec, x_wec, x_opt, waves, nsubsteps)\n", " return f_max - np.abs(f.flatten())\n", "\n", "ineq_cons = {'type': 'ineq',\n", @@ -974,7 +974,7 @@ " f_max = 750.0\n", "\n", " def const_f_pto(wec, x_wec, x_opt, waves):\n", - " f = pto.force_on_wec(wec, x_wec, x_opt, waves, nsubsteps)\n", + " f = pto.force(wec, x_wec, x_opt, waves, nsubsteps)\n", " return f_max - np.abs(f.flatten())\n", "\n", " ineq_cons = {'type': 'ineq', 'fun': const_f_pto}\n", diff --git a/wecopttool/pto.py b/wecopttool/pto.py index 17841ec9c..7ff85db55 100644 --- a/wecopttool/pto.py +++ b/wecopttool/pto.py @@ -745,7 +745,7 @@ def _postproc(wec, res, waves, nsubsteps): vel_attr = {'long_name': 'Velocity', 'units': 'm/s or rad/s'} acc_attr = {'long_name': 'Acceleration', 'units': 'm/s^2 or rad/s^2'} - force_attr = {'long_name': 'Force or moment on WEC', + force_attr = {'long_name': 'PTO force or torque', 'units': 'N or Nm'} power_attr = {'long_name': 'Power', 'units': 'W'} mech_power_attr = {'long_name': 'Mechanical power', 'units': 'W'} From 0ed47048701a61a5a38f65d63d3beadbecbb196d Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Thu, 10 Oct 2024 14:59:51 -0600 Subject: [PATCH 21/27] Add debug option for building documentation locally (#374) * added production and debug options for docs build * limited choices for input options --- .github/workflows/pr.yml | 2 +- .github/workflows/push.yml | 2 +- .github/workflows/release.yml | 2 +- docs/build_docs.py | 65 ++++++++++++++++++++++------------- 4 files changed, 45 insertions(+), 26 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 14827dde1..4ab47fc1f 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -139,4 +139,4 @@ jobs: shell: bash -l {0} run: | git fetch --tags - python3 docs/build_docs.py + python3 docs/build_docs.py --build production diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 0f4edd377..246ff62f5 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -138,7 +138,7 @@ jobs: shell: bash -l {0} run: | git fetch --tags - python3 docs/build_docs.py + python3 docs/build_docs.py --build production - name: Deploy uses: peaceiris/actions-gh-pages@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc9914e58..e0aa7ec03 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,7 @@ jobs: shell: bash -l {0} run: | git fetch --tags - python3 docs/build_docs.py + python3 docs/build_docs.py --build production - name: Deploy uses: peaceiris/actions-gh-pages@v3 diff --git a/docs/build_docs.py b/docs/build_docs.py index 006882478..78b818a2f 100644 --- a/docs/build_docs.py +++ b/docs/build_docs.py @@ -3,6 +3,7 @@ import shutil import re import yaml +import argparse from sphinx.application import Sphinx @@ -17,6 +18,10 @@ html_dir = os.path.join(build_dir, 'html') doctree_dir = os.path.join(build_dir, 'doctrees') +parser = argparse.ArgumentParser() +parser.add_argument('-b', '--build', nargs=1, type=str, + choices=['debug', 'production'], + default='debug') def linkcheck(): app = Sphinx(source_dir, @@ -51,36 +56,50 @@ def cleanup(): f.write(data2) -def build_doc(version, tag, home_branch): - os.environ['current_version'] = version - subprocess.run(f'git checkout {tag}', shell=True) - subprocess.run( - f"git checkout {home_branch} -- {os.path.join(source_dir, 'conf.py')}", shell=True) - subprocess.run( - f"git checkout {home_branch} -- {os.path.join(docs_dir, 'versions.yaml')}", shell=True) - subprocess.run( - f"git checkout {home_branch} -- {os.path.join(source_dir, '_templates/versions.html')}", shell=True) +def build_doc(version, tag, home_branch, build): + if build != 'debug': + os.environ['current_version'] = version + subprocess.run(f'git checkout {tag}', shell=True) + subprocess.run( + f"git checkout {home_branch} -- {os.path.join(source_dir, 'conf.py')}", + shell=True) + subprocess.run( + f"git checkout {home_branch} -- {os.path.join(docs_dir, 'versions.yaml')}", + shell=True) + subprocess.run( + f"git checkout {home_branch} -- {os.path.join(source_dir, '_templates/versions.html')}", + shell=True) source.make_theory_animations linkcheck() html() cleanup() - subprocess.run( - f"git checkout {home_branch}", shell=True) + if build != 'debug': + subprocess.run( + f"git checkout {home_branch}", shell=True) -if __name__ == '__main__': - home_name = 'latest' - with open(os.path.join(docs_dir, 'versions.yaml'), 'r') as v_file: - versions = yaml.safe_load(v_file) - home_branch = versions[home_name] - build_doc(home_name, home_branch, home_branch) +def move_pages(): print(f"Moving HTML pages to {os.path.join(docs_dir, 'pages')}...") shutil.copytree(html_dir, os.path.join(docs_dir, 'pages')) print('Done.') - for name, tag in versions.items(): - if name != home_name: - build_doc(name, tag, home_branch) - print(f"Moving HTML pages to {os.path.join(docs_dir, 'pages', name)}...") - shutil.copytree(html_dir, os.path.join(docs_dir, 'pages', name)) - print('Done.') + + +if __name__ == '__main__': + args = parser.parse_args() + build = args.build + if build == 'debug': + print(f'Building docs in current branch...') + build_doc('latest', '', '', build) + move_pages() + else: + home_name = 'latest' + with open(os.path.join(docs_dir, 'versions.yaml'), 'r') as v_file: + versions = yaml.safe_load(v_file) + home_branch = versions[home_name] + build_doc(home_name, home_branch, home_branch, build) + move_pages() + for name, tag in versions.items(): + if name != home_name: + build_doc(name, tag, home_branch) + move_pages() shutil.rmtree(html_dir) From 2c3c9abeb52e96e7078ac5a83b7cd6d304857464 Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Fri, 11 Oct 2024 11:13:02 -0600 Subject: [PATCH 22/27] Adding Pioneer journal paper to reference list (#372) * added reference to Devin et al. 2024 (Pioneer journal paper) * use DOI without "doi.org" prefix --------- Co-authored-by: Ryan Coe --- docs/source/references.rst | 1 + docs/source/wecopttool_refs.bib | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/docs/source/references.rst b/docs/source/references.rst index dcda36f82..ac737676d 100644 --- a/docs/source/references.rst +++ b/docs/source/references.rst @@ -21,3 +21,4 @@ Bibliography Grasberger:2023aa Strofer:2023vw Coe2020Initial + DEVIN2024119124 diff --git a/docs/source/wecopttool_refs.bib b/docs/source/wecopttool_refs.bib index 21e553e2d..2c8adf69b 100644 --- a/docs/source/wecopttool_refs.bib +++ b/docs/source/wecopttool_refs.bib @@ -99,3 +99,13 @@ @article{Bacelli2014Numerical title = {Numerical optimal control of wave energy converters}, volume = {6}, year = {2014}} + +@article{DEVIN2024119124, + title = {High-dimensional control co-design of a wave energy converter with a novel pitch resonator power takeoff system}, + author = {Michael C. Devin and Daniel T. Gaebele and Carlos A. Michelén Ströfer and Jeff T. Grasberger and Jantzen Lee and Ryan G. Coe and Giorgio Bacelli}, + journal = {Ocean Engineering}, + volume = {312}, + pages = {119124}, + year = {2024}, + issn = {0029-8018}, + doi = {10.1016/j.oceaneng.2024.119124}} From 8e7a325903c41172c85673bc284ecfe5b4dfde5f Mon Sep 17 00:00:00 2001 From: jtgrasb <87095491+jtgrasb@users.noreply.github.com> Date: Thu, 17 Oct 2024 12:22:19 -0600 Subject: [PATCH 23/27] Fix pioneer tutorial bug (define omega_data) (#379) --- examples/tutorial_4_Pioneer.ipynb | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/tutorial_4_Pioneer.ipynb b/examples/tutorial_4_Pioneer.ipynb index 45cc13b2f..cd7d5b066 100644 --- a/examples/tutorial_4_Pioneer.ipynb +++ b/examples/tutorial_4_Pioneer.ipynb @@ -306,6 +306,7 @@ "datafile = 'data/pioneer_empirical_data.nc'\n", "empirical_data = xr.load_dataset(datafile)\n", "\n", + "omega_data = empirical_data.omega\n", "exc_coeff_data = empirical_data.exc_coeff_data_real + 1j*empirical_data.exc_coeff_data_imag\n", "Zi_data = empirical_data.Zi_data_real + 1j*empirical_data.Zi_data_imag\n", "Zi_stiffness = empirical_data.Zi_stiffness\n", From 2c0ef7ba430c8f3a1fb3bc2ae10276abf293d76a Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Thu, 17 Oct 2024 16:08:52 -0600 Subject: [PATCH 24/27] Fixing typo from #374 (#375) * added missing input argument * fixed HTML pages destination folder not being version specific --- docs/build_docs.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/docs/build_docs.py b/docs/build_docs.py index 78b818a2f..e8f4f231b 100644 --- a/docs/build_docs.py +++ b/docs/build_docs.py @@ -78,9 +78,15 @@ def build_doc(version, tag, home_branch, build): f"git checkout {home_branch}", shell=True) -def move_pages(): - print(f"Moving HTML pages to {os.path.join(docs_dir, 'pages')}...") - shutil.copytree(html_dir, os.path.join(docs_dir, 'pages')) +def move_pages(dest_dir=None): + if dest_dir is None: + print(f"Moving HTML pages to {os.path.join(docs_dir, 'pages')}...") + shutil.copytree( + html_dir, os.path.join(docs_dir, 'pages')) + else: + print(f"Moving HTML pages to {os.path.join(docs_dir, 'pages', dest_dir)}...") + shutil.copytree( + html_dir, os.path.join(docs_dir, 'pages', dest_dir)) print('Done.') @@ -100,6 +106,6 @@ def move_pages(): move_pages() for name, tag in versions.items(): if name != home_name: - build_doc(name, tag, home_branch) - move_pages() + build_doc(name, tag, home_branch, build) + move_pages(dest_dir=name) shutil.rmtree(html_dir) From 71c826abfc290a705d8b9c6e30c5a29729c27fc7 Mon Sep 17 00:00:00 2001 From: jtgrasb <87095491+jtgrasb@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:46:59 -0600 Subject: [PATCH 25/27] Test capytaine lids (#363) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update CONTRIBUTING.md to indicate PRs should be to the new `dev` branch * Update RELEASING.md to reflect new workflow with the `dev` branch * Set Default uniform_shift to False (#313) * Make uniform shift default false and fix test_core * Allow for non-uniform shift for impedance * update docstrings (#326) * damping naming and consistently change radiation damping (#328) * run CI + codeQL on dev PRs/pushses * Fix test failures (#330) * fix tests * fix pioneer * actually test power solution for irregular wave (#327) * Update test_integration with new scaling (#333) * Phase Realizations Demo and Docs (#315) * Pioneer tutorial realizations demo Add frequency array and realization study to beginning of pioneer tutorial * Update pioneer * Clean pioneer and update docs * remove print * Units * Update pioneer plots * Waves before frequencies * Explain less frequencies * Minor updates * Update pioneer * Merge branch 'dev' of https://github.com/sandialabs/WecOptTool into phases_demo * issue 321 fd_to_td() bug (#329) * bug bix : DC and Nyquist frequency should not be devided by two before ifft * Changed td_to_fd to scale single sided frequency components rather than TD signal * minor bug fix from issue332 #332 * nodf -> ndof (#334) * add DOI for Daniel's paper (#336) * Tutorial cleanup (#339) * run CI on PRs against dev branch * revamped tutorial 1, including fix for #293 * more tutorial cleanup and editorial changes * more cleanup and incorporated changes in #315 * fixed tutorial 2 colormaps * finishing touches * reverted a few accidental changes * fixes as per Jeff's review comments --------- Co-authored-by: Ryan Coe * Utils dev - replaces old utilities PR (#343) * run CI on PRs against dev branch * coppied fundamental utility files * import utilities module * added utilities funtions to tut1 * added bem plot from utils * added bem plot from utils * updated sankey plot * updated check_radiation_damping * cleared outputs * corrected bug * changed Zi to hydro_impedance to be consistent with our variables name python convention * PR review edits * add grid to plots * removed draft functions in utilities.py * typo * Fixed one more typo I found while reviewing Daniel's changes --------- Co-authored-by: Ryan Coe Co-authored-by: Michael Devin * Post process docstrings; use loops for multiple realizations (#337) * post_processing docstrings - examples - parameters (order) * handle multiple phase realizations internally * Update wecopttool/core.py * making outputs lists * Update implementation to function with tutorial 1 for now * Update tutorials * Update LUPA * Make sure same WEC is passed in * Add test_utilities Revert "Add test_utilities" This reverts commit 27399f03320f2c69fcb641e1496521b40c4a9d0b. * Update utilities module * Update utilities * Update test_utilities * Update tutorial 1 utilities call --------- Co-authored-by: Carlos A. Michelén Ströfer Co-authored-by: Carlos A. Michelén Ströfer Co-authored-by: jtgrasb Co-authored-by: jtgrasb <87095491+jtgrasb@users.noreply.github.com> * v3.0.0 * Lower tolerance for new test to fix CI failing occasionally * hyperlinks no longer have formatting, plus other small adjustments (#348) * Merge to dev, not main (#349) * Dev version of documentation site (#347) * added initial file changes based on sphinx_multiversion docs and WEC-Sim implementation * removed sphinx-multiversion since it is no longer supported and made manual multiversion * now uses absolute paths, commented out linkcheck for debugging * fixed docstring errors in utilities module * updating files again that somehow got reverted * fixing path in conf.py * don't run tutorials (will revert later) * handle file moves correctly, fixed if statement to make other versions appear * fixed two bugs in versions template * reverted temp changes, changes latest to main * switched latest to main * main branch now in root directory of pages * fixed URLs with change from last commit * make other branches visible before building * switched main branch tag for more testing * fixed typo * switched dev branch to an existing branch * renamed main to latest, changed version.html file name to avoid confusion * added prints about moving files so Sphinx output isn't misleading * fixed typo with quotations * changed versions.html name back because that broke things I guess * modified contributing documentation to reflect changes * add logic to remove duplicate 'latest' branch * Fixed pathing when already on latest * remove typo * Troubleshooting complete, switching back to correct branches for deployment * Removed extra word in docstring * removed redundant function * fixed pathing so returns to same file (and fixes tutorial/API docs) * changed latest branch for demonstration * switched back latest branch for deployment * updated with new Capytaine docs URL * Add warnings when adding inertia and hydrostatic stiffness automatically (#346) * CI workflow cleanup (#352) * removed conda environment from workflows since newer capytaine/wavespectra work with Windows * fixed unnecessary capitalization * still create CI conda environment to fix Mac environment failures * added conda env fully back in, push workflow deploys docs, split PR workflow * conda environment activates again * mambaforge instead of miniforge * manual cache reset * reset to older version of setup-miniconda to troubleshoot * Specify NumPy version <2.0 since it breaks autograd * v3.0.1 (#360) * Revert "v3.0.1 (#360)" (#361) This reverts commit f79ff8b97bde9c674b921a526ce261db4f8f299b. * Update pyproject.toml * Test out lid feature * Add lid to pioneer and remove extra files * Revert LUPA and add lid comment * Revert LUPA bem data * Fix pioneer * Test lid LUPA * Update LUPA with lid * LUPA lid * Revert LUPA unnecessary changes * Resolve Daniel's suggestions --------- Co-authored-by: Carlos A. Michelén Ströfer Co-authored-by: Ryan Coe Co-authored-by: Daniel Gaebele <86246113+dtgaebe@users.noreply.github.com> Co-authored-by: Michael Devin Co-authored-by: Carlos A. Michelén Ströfer Co-authored-by: mcdevin --- examples/data/bem.nc | Bin 39290 -> 44430 bytes examples/tutorial_1_WaveBot.ipynb | 16 +++++++++--- examples/tutorial_2_AquaHarmonics.ipynb | 9 +++++-- examples/tutorial_3_LUPA.ipynb | 32 ++++++++++++++---------- examples/tutorial_4_Pioneer.ipynb | 8 ++++-- 5 files changed, 45 insertions(+), 20 deletions(-) diff --git a/examples/data/bem.nc b/examples/data/bem.nc index bca7066d50a889a512b2855b0431960bc0dd06de..45be488d8b060f320d1d86565dae8041a0f5ab9b 100644 GIT binary patch literal 44430 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 z%0A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 Date: Mon, 13 Jan 2025 18:13:57 -0700 Subject: [PATCH 26/27] updated to allow NumPy v2.0+, updated a few functions that had been removed in newer numpy versions --- pyproject.toml | 2 +- tests/test_integration.py | 2 +- wecopttool/core.py | 10 +++++----- wecopttool/pto.py | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 9b9440e73..63376d8b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ - "numpy>=1.20, <2.0", + "numpy>=1.20", "scipy", "xarray", "autograd", diff --git a/tests/test_integration.py b/tests/test_integration.py index 739956d0d..606188640 100644 --- a/tests/test_integration.py +++ b/tests/test_integration.py @@ -301,7 +301,7 @@ def test_p_controller_resonant_wave(self, scale_x_opt=1e-3, scale_obj=1e-1, optim_options={'ftol': 1e-10}, - bounds_opt=((-1*np.infty, 0),), + bounds_opt=((-1*np.inf, 0),), ) power_sol = -1*res[0]['fun'] diff --git a/wecopttool/core.py b/wecopttool/core.py index 40ca2a1be..8c10a2d31 100644 --- a/wecopttool/core.py +++ b/wecopttool/core.py @@ -58,7 +58,7 @@ from typing import Iterable, Callable, Any, Optional, Mapping, TypeVar, Union from pathlib import Path import warnings -from datetime import datetime +import datetime from numpy.typing import ArrayLike import autograd.numpy as np @@ -80,7 +80,7 @@ warnings.filterwarnings("ignore", message=filter_msg) # default values -_default_parameters = {'rho': 1025.0, 'g': 9.81, 'depth': np.infty} +_default_parameters = {'rho': 1025.0, 'g': 9.81, 'depth': np.inf} _default_min_damping = 1e-6 # type aliases @@ -895,7 +895,7 @@ def post_process(self, "post-process and be passed as an input.") def _postproc(res, waves, nsubsteps): - create_time = f"{datetime.utcnow()}" + create_time = f"{datetime.datetime.now(datetime.UTC)}" omega_vals = np.concatenate([[0], waves.omega.values]) freq_vals = np.concatenate([[0], waves.freq.values]) @@ -1060,7 +1060,7 @@ def omega(self) -> ndarray: @property def period(self) -> ndarray: """Period vector [s].""" - return np.concatenate([[np.Infinity], 1/self._freq[1:]]) + return np.concatenate([[np.inf], 1/self._freq[1:]]) @property def w1(self) -> float: @@ -2109,7 +2109,7 @@ def standard_forces(hydro_data: Dataset) -> TForceDict: def run_bem( fb: cpy.FloatingBody, - freq: Iterable[float] = [np.infty], + freq: Iterable[float] = [np.inf], wave_dirs: Iterable[float] = [0], rho: float = _default_parameters['rho'], g: float = _default_parameters['g'], diff --git a/wecopttool/pto.py b/wecopttool/pto.py index 7ff85db55..050f42ff4 100644 --- a/wecopttool/pto.py +++ b/wecopttool/pto.py @@ -34,7 +34,7 @@ from scipy.linalg import block_diag from scipy.optimize import OptimizeResult from xarray import DataArray, Dataset -from datetime import datetime +import datetime from scipy.optimize import OptimizeResult from wecopttool.core import complex_to_real, td_to_fd @@ -707,7 +707,7 @@ def post_process(self, """ def _postproc(wec, res, waves, nsubsteps): - create_time = f"{datetime.utcnow()}" + create_time = f"{datetime.datetime.now(datetime.UTC)}" x_wec, x_opt = wec.decompose_state(res.x) From 07b9666dd5b290662013b13f66d2f50753973b22 Mon Sep 17 00:00:00 2001 From: Michael Devin Date: Mon, 27 Jan 2025 12:10:48 -0700 Subject: [PATCH 27/27] Use Miniforge distribution in CI (#392) * switch to Miniforge distribution since Mambaforge is no longer supported * updated name * updated to newer GitHub Action version * changed miniforge variant * switched to v2 of GH Action since v3 defaults to osx-arm64 --- .github/workflows/pr.yml | 6 ++---- .github/workflows/push.yml | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 4ab47fc1f..77879f004 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -30,10 +30,10 @@ jobs: # - the content of pyproject.toml changes # - you manually change the value of the CACHE_NUMBER below # Else the existing cache is used. - - name: Setup Mambaforge + - name: Setup Miniforge uses: conda-incubator/setup-miniconda@v2 with: - miniforge-variant: Mambaforge + miniforge-variant: Miniforge3 miniforge-version: latest activate-environment: test-env use-mamba: true @@ -45,8 +45,6 @@ jobs: shell: bash # create a conda yaml file - # for some reason Windows installs capytaine 1.4 instead of latest. - # CHANGE: Capytaine version - name: Create environment.yml file run: | echo "name: test-env" >> environment.yml; diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 246ff62f5..260ded386 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -30,10 +30,10 @@ jobs: # - the content of pyproject.toml changes # - you manually change the value of the CACHE_NUMBER below # Else the existing cache is used. - - name: Setup Mambaforge + - name: Setup Miniforge uses: conda-incubator/setup-miniconda@v2 with: - miniforge-variant: Mambaforge + miniforge-variant: Miniforge3 miniforge-version: latest activate-environment: test-env use-mamba: true @@ -45,8 +45,6 @@ jobs: shell: bash # create a conda yaml file - # for some reason Windows installs capytaine 1.4 instead of latest. - # CHANGE: Capytaine version - name: Create environment.yml file run: | echo "name: test-env" >> environment.yml;