diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 94cfc8ae5..0790d7cba 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -33,10 +33,9 @@ pip install -e .[dev] * Docstrings: [pep257](https://www.python.org/dev/peps/pep-0257/) & [NumPy style](https://numpydoc.readthedocs.io/en/latest/format.html). * Type hints: [module documentation](https://docs.python.org/3/library/typing.html), [cheat sheet](https://mypy.readthedocs.io/en/stable/cheat_sheet_py3.html). -## Autograd -This project uses [`autograd`](https://github.com/HIPS/autograd) for automatic differentiation. -Autograd does not support all NumPy and SciPy functionalities, see [autograd documentation](https://github.com/HIPS/autograd/blob/master/docs/tutorial.md#supported-and-unsupported-parts-of-numpyscipy). -*NOTE:* using unsupported functionalities results in the gradient calculation failing silently. +## Jax +This project uses [`Jax`](https://github.com/jax-ml/jax) for automatic differentiation. +Jax does not support all NumPy and SciPy functionalities, see [jax documentation](https://docs.jax.dev/en/latest/notebooks/Common_Gotchas_in_JAX.html). ## Pull Requests 1. Create a fork of WecOptTool diff --git a/docs/clean_docs.py b/docs/clean_docs.py index 0d152aa05..a91db2fcc 100644 --- a/docs/clean_docs.py +++ b/docs/clean_docs.py @@ -22,7 +22,10 @@ def clean() -> None: _assert_within_docs(target) if os.path.exists(target): print(f"Removing {target}") - rmtree(target, ignore_errors=False) + try: + rmtree(target, ignore_errors=False) + except Exception as err: + print(f"Failed to remove {target}: {err}") else: print(f"Skipping {target} (not found)") diff --git a/docs/source/_static/pseudospectral_equivalence.png b/docs/source/_static/pseudospectral_equivalence.png new file mode 100644 index 000000000..00edd648b Binary files /dev/null and b/docs/source/_static/pseudospectral_equivalence.png differ diff --git a/docs/source/_static/wavebot_ex_xopt_iterates.gif b/docs/source/_static/wavebot_ex_xopt_iterates.gif new file mode 100644 index 000000000..d18e5b543 Binary files /dev/null and b/docs/source/_static/wavebot_ex_xopt_iterates.gif differ diff --git a/docs/source/conf.py b/docs/source/conf.py index c7cd959da..136cb14bb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -94,6 +94,10 @@ def _resolve_version_context(config=None, current_ref_override=None) -> tuple[st skip_notebook_execution = os.environ.get('WOT_DOCS_SKIP_NOTEBOOK_EXECUTION') == '1' +skip_theory_animations = os.environ.get('WOT_DOCS_SKIP_THEORY_ANIMATIONS') == '1' +skip_pseudospectral_visualizations = os.environ.get( + 'WOT_DOCS_SKIP_PSEUDOSPECTRAL_VISUALIZATIONS' +) == '1' # -- General configuration --------------------------------------------------- extensions = [ @@ -137,17 +141,19 @@ def _all_but_nc(_dir, contents): def _copy_examples() -> None: print('Copy example notebooks into docs/_examples') examples_dst = os.path.join(project_root, 'docs/source/_examples') - shutil.rmtree(examples_dst, ignore_errors=True) + os.makedirs(examples_dst, exist_ok=True) + shutil.copytree( os.path.join(project_root, 'examples'), examples_dst, ignore=_all_but_ipynb, - dirs_exist_ok=True + dirs_exist_ok=True, ) shutil.copytree( os.path.join(project_root, 'examples/data'), - os.path.join(project_root, 'docs/source/_examples/data'), + os.path.join(examples_dst, 'data'), ignore=_all_but_nc, + dirs_exist_ok=True, ) @@ -165,6 +171,20 @@ def _generate_theory_animations() -> None: _theory_animations_generated = True +def _generate_pseudospectral_visualizations() -> None: + global _pseudospectral_visualizations_generated + if _pseudospectral_visualizations_generated: + return + + importlib.invalidate_caches() + module_name = 'make_pseudospectral_visualizations' + if module_name in sys.modules: + importlib.reload(sys.modules[module_name]) + else: + importlib.import_module(module_name) + _pseudospectral_visualizations_generated = True + + def _cleanup_index_html(outdir: str) -> None: index_file = os.path.join(outdir, 'index.html') if not os.path.exists(index_file): @@ -195,8 +215,14 @@ def _on_config_inited(_app, _config): if skip_notebook_execution: print('Skipping notebook execution') - - _generate_theory_animations() + if skip_theory_animations: + print('Skipping theory animation generation') + else: + _generate_theory_animations() + if skip_pseudospectral_visualizations: + print('Skipping pseudospectral visualization generation') + else: + _generate_pseudospectral_visualizations() def _on_build_finished(app, exception): @@ -212,6 +238,7 @@ def setup(app): _theory_animations_generated = False +_pseudospectral_visualizations_generated = False suppress_warnings = ['autosectionlabel.*', # nbsphinx and austosectionlabel do not play well together 'app.add_node', # using multiple builders in custom Sphinx objects throws a bunch of these diff --git a/docs/source/iterate_history_wavebot_ex1_elec.npz b/docs/source/iterate_history_wavebot_ex1_elec.npz new file mode 100644 index 000000000..e1d784b5f Binary files /dev/null and b/docs/source/iterate_history_wavebot_ex1_elec.npz differ diff --git a/docs/source/make_pseudospectral_visualizations.py b/docs/source/make_pseudospectral_visualizations.py new file mode 100644 index 000000000..a95edec4d --- /dev/null +++ b/docs/source/make_pseudospectral_visualizations.py @@ -0,0 +1,259 @@ +# docs/source/make_pseudospectral_visualizations.py + +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import matplotlib as mpl +import matplotlib.pyplot as plt +from matplotlib.animation import FuncAnimation, PillowWriter + +import wecopttool as wot + + +mpl.rcParams.update( + { + "font.family": "serif", + "mathtext.fontset": "cm", + "mathtext.rm": "serif", + "axes.unicode_minus": False, + } +) + +HERE = Path(__file__).resolve().parent +STATIC_DIR = HERE / "_static" +STATIC_DIR.mkdir(parents=True, exist_ok=True) + + +def generate_frequency_domain_signal(nfreq: int, Np: int = 4, seed: int = 17, sigma: float = 0.55) -> np.ndarray: + k = np.arange(nfreq + 1) + mu = np.log(Np) + sigma**2 + + mag = np.zeros_like(k, dtype=float) + kk = k[1:] + mag[1:] = (1 / (kk * sigma * np.sqrt(2 * np.pi))) * np.exp(-(np.log(kk) - mu) ** 2 / (2 * sigma**2)) + + rng = np.random.default_rng(seed) + phase = rng.uniform(-np.pi, np.pi, size=nfreq + 1) + phase[0] = 0.0 + phase[-1] = 0.0 + + X_fd_1d = mag * np.exp(1j * phase) + X_fd_1d[0] = np.real(X_fd_1d[0]) + X_fd_1d[-1] = np.real(X_fd_1d[-1]) + + return X_fd_1d[:, None] + + +def build_pseudospectral_figure( + f1: float, + nfreq: int, + X_fd: np.ndarray, + nsub: int = 10, + fig=None, + ax=None, + zlim=None, + zero_freq: bool = False, + title_str: str | None = None, + plot_collocation_points: bool = True, +): + freqs = wot.frequency(f1, nfreq, zero_freq=zero_freq) + t = wot.time(f1, nfreq) + t_full = np.insert(t, 2 * nfreq, 1 / freqs[1]) + t_sub = wot.time(f1, nfreq, nsubsteps=nsub) + + x_td = wot.fd_to_td(X_fd, f1=f1, nfreq=nfreq, zero_freq=zero_freq).squeeze() + x_td_interp = wot.fd_to_td(X_fd, f1=f1, nfreq=nfreq, nsubsteps=nsub, zero_freq=zero_freq).squeeze() + + xk_td = [] + for k in range(X_fd.shape[0]): + Xk = np.zeros_like(X_fd) + Xk[k, 0] = X_fd[k, 0] + xk_td.append(wot.fd_to_td(Xk, f1=f1, nfreq=nfreq, nsubsteps=nsub, zero_freq=zero_freq).squeeze()) + xk_td = np.array(xk_td) + + if fig is None or ax is None: + fig = plt.figure(figsize=(6, 5)) + ax = fig.add_subplot(111, projection="3d") + else: + ax.cla() + + if zlim is not None: + ax.set_zlim(zlim[0], zlim[1]) + + dxaxis = 0.35 + y_xax_maj = -dxaxis * freqs[-1] + y_xax_min = (1 + dxaxis) * freqs[-1] + ax.set_ylim(y_xax_maj, y_xax_min * 0.8) + + dyaxis = 0.35 + t_freq_ax_mag = (1 + dyaxis) * t[-1] + t_freq_ax_ph = -t_freq_ax_mag + t[-1] + ax.set_xlim(-0.02 * t[-1], t_freq_ax_mag) + + ax.xaxis.pane.set_visible(False) + ax.yaxis.pane.set_visible(False) + ax.zaxis.pane.set_visible(False) + ax.grid(False) + ax.set_axis_off() + + xmin, xmax = -0.05 * t[-1], 1.05 * t[-1] + + ax.plot([xmin, xmax], [y_xax_maj, y_xax_maj], [0, 0], color="k", lw=1) + ax.plot(t_full, np.full_like(t_full, y_xax_maj), np.zeros_like(t_full), color="b", linestyle="None", alpha=0.4, marker="|", markersize=4) + ax.plot([xmin, xmax], [y_xax_min] * 2, [0, 0], color="k", lw=1) + ax.plot(t_full, np.full_like(t_full, y_xax_min), np.zeros_like(t_full), color="b", linestyle="None", alpha=0.4, marker="|", markersize=4) + + ax.text(1 / freqs[1], y_xax_min, 0, r"$T=\frac{1}{f_1}$", ha="left", va="center") + if zero_freq: + ax.text(0, y_xax_min, 0, r"0", ha="left", va="bottom") + ax.text(t[1], y_xax_min, 0, r"$\Delta t = \frac{1}{2 n_{freq} f_1}$", "x", ha="left", va="center") + ax.text((t[-1] - t[1]) / 2, y_xax_maj, 0, r"$x_t$", "x", ha="center", va="top") + + ax.plot([t_freq_ax_mag] * 2, np.array([-0.05, 1.05]) * freqs[-1], [0, 0], color="k", lw=1) + ax.plot(np.full_like(freqs, t_freq_ax_mag), freqs, np.zeros_like(freqs), color="k", linestyle="None", alpha=0.4, marker="|", markersize=4) + ax.plot([t_freq_ax_ph] * 2, np.array([-0.05, 1.05]) * freqs[-1], [0, 0], color="k", lw=1) + ax.plot(np.full_like(freqs, t_freq_ax_ph), freqs, np.zeros_like(freqs), color="k", linestyle="None", alpha=0.4, marker="|", markersize=4) + + ax.plot(np.full_like(freqs, t_freq_ax_mag), freqs, np.abs(X_fd.squeeze()), color="k", lw=1.0, linestyle=":") + z_ph_ax = zlim[1] / 3 if zlim is not None else 1.0 + ax.plot(np.full_like(freqs, t_freq_ax_ph), freqs, z_ph_ax, color="k", lw=0.7, linestyle="--") + ax.plot(np.full_like(freqs, t_freq_ax_ph), freqs, -z_ph_ax, color="k", lw=0.7, linestyle="--") + + ax.text(t_freq_ax_mag, freqs[1], 0, r"$f_1$", ha="left", va="top") + ax.text(t_freq_ax_mag, freqs[-1], 0, r"$n_{freq} f_1$", ha="left", va="top") + ax.text(t_freq_ax_mag * 1.1, freqs[-1] / 2, 0, r"$|X_k|$", "y", ha="center", va="top") + ax.text(t_freq_ax_ph - 0.1 * t_freq_ax_mag, freqs[-1] / 2, z_ph_ax, r"$\measuredangle X_k$", "y", ha="center", va="top") + + for k in range(nfreq + 1): + y = np.full_like(t_sub, freqs[k]) + (ln,) = ax.plot(t_sub, y, xk_td[k], lw=1.0, alpha=0.7) + ax.plot([t_freq_ax_mag] * 2, [freqs[k]] * 2, [0, np.abs(X_fd[k, 0])], color=ln.get_color(), lw=1.0) + ax.scatter(t_sub[-1], freqs[k], xk_td[k][-1], color=ln.get_color(), s=5, facecolors="none") + ax.scatter(t_freq_ax_ph, freqs[k], np.angle(X_fd[k, 0]) / np.pi * z_ph_ax, color=ln.get_color(), s=6) + ax.plot([t[0], t[-1]], [freqs[k]] * 2, [0, 0], color="b", lw=0.4, alpha=0.25, linestyle="--") + ax.plot([t_freq_ax_ph] * 2, [freqs[k]] * 2, [-z_ph_ax, z_ph_ax], color=ln.get_color(), linestyle="--", lw=0.7) + if plot_collocation_points: + (clpts,) = ax.plot( + t, + np.full_like(t, freqs[k]), + xk_td[k, ::nsub], + color="b", + linestyle="None", + marker="s", + markerfacecolor="None", + markersize=1, + alpha=0.7, + ) + clpts.set_zorder(1) + + for n in t: + ax.plot([n] * 2, [freqs[0], freqs[-1]], [0, 0], color="b", lw=0.4, alpha=0.25, linestyle="--") + + ax.plot(t, np.full_like(t, y_xax_maj), x_td, color="k", lw=1, linestyle="--") + ax.plot(t, np.full_like(t, y_xax_maj), x_td, color="b", linestyle="None", marker="s", markerfacecolor="None", markersize=3) + ax.plot(t_sub, np.full_like(t_sub, y_xax_maj), x_td_interp, color="k", lw=0.7, linestyle="-.", alpha=0.7) + + if title_str is not None: + ax.set_title(title_str, pad=2) + + plt.tight_layout() + return fig, ax + + +def animate_xopt_history( + x_opt: np.ndarray, + f1: float, + nfreq: int, + outfile: Path, + fps: int = 1, + nsub: int = 10, + zlim: tuple[float, float] | None = None, + title_prefix: str = "WecOptTool Tutorial 1:\nWaveBot electrical power\nIteration:", + dpi: int = 150, +): + fig = plt.figure(figsize=(6, 6)) + ax = fig.add_subplot(111, projection="3d") + + def row_to_Xfd(row): + x_opt_mat = row[:, None] + X_fd = np.array(wot.real_to_complex(x_opt_mat, zero_freq=True)) + X_fd[0, 0] = np.real(X_fd[0, 0]) + X_fd[-1, 0] = np.real(X_fd[-1, 0]) + return X_fd + + def update(i): + X_fd = row_to_Xfd(x_opt[i, :]) + title_str = rf"{title_prefix} {i+1}/{x_opt.shape[0]}" "\n" rf"$f_1={f1:.4g}\ \mathrm{{Hz}},\ n_{{freq}}={nfreq}$" + build_pseudospectral_figure( + f1=f1, + nfreq=nfreq, + X_fd=X_fd, + zero_freq=True, + nsub=nsub, + fig=fig, + ax=ax, + zlim=zlim, + title_str=title_str, + plot_collocation_points=False, + ) + return [] + + anim = FuncAnimation(fig, update, frames=x_opt.shape[0], interval=int(1000 / fps), blit=False) + anim.save(str(outfile), writer=PillowWriter(fps=fps), dpi=dpi) + plt.close(fig) + + +def make_equivalence_figure(): + f1 = 0.02 + nfreq = 5 + X_fd = generate_frequency_domain_signal(nfreq, Np=3, seed=9) + fig, _ax = build_pseudospectral_figure(f1=f1, nfreq=nfreq, X_fd=X_fd, zero_freq=True, nsub=10, zlim=(-0.7, 0.7)) + fig.savefig( + STATIC_DIR / "pseudospectral_equivalence.png", + dpi=300, + bbox_inches="tight", + pad_inches=0.02, + ) + plt.close(fig) + + +def make_wavebot_animation(): + # The WaveBot animation was generated from iterate_history_wavebot_ex1_elec.npz + # derived from Tutorial 1 (objective electrical power) optimization history. + npz_path = HERE / "iterate_history_wavebot_ex1_elec.npz" + if not npz_path.exists(): + raise FileNotFoundError( + f"Required file not found: {npz_path}\n" + "Place iterate_history_wavebot_ex1_elec.npz in docs/source/ before running this script." + ) + + data = np.load(npz_path, allow_pickle=True) + f1_sol = float(data["f1"]) + nfreq_sol = int(data["nfreq"]) + x_opt = data["x_opt"] + data.close() + + animate_xopt_history( + x_opt=x_opt, + f1=f1_sol, + nfreq=nfreq_sol, + outfile=STATIC_DIR / "wavebot_ex_xopt_iterates.gif", + fps=1, + nsub=10, + zlim=(-750, 750), + dpi=150, + ) + + +def main(): + make_equivalence_figure() + make_wavebot_animation() + print("Wrote:") + print(f" {STATIC_DIR / 'pseudospectral_equivalence.png'}") + print(f" {STATIC_DIR / 'wavebot_ex_xopt_iterates.gif'}") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/docs/source/make_theory_animations.py b/docs/source/make_theory_animations.py index 16c0c5135..17349d710 100644 --- a/docs/source/make_theory_animations.py +++ b/docs/source/make_theory_animations.py @@ -23,6 +23,8 @@ M = 10 # number of times to repeat final frame # will produce M+N total frames +skip_gifsicle = os.environ.get('WOT_DOCS_SKIP_GIFSICLE') == '1' + with tempfile.TemporaryDirectory() as tmpdirname: fig, ax = plt.subplots() @@ -102,4 +104,6 @@ axis=0) gif_path = os.path.join(odir, f"{gif_name}_{key}.gif") iio.imwrite(gif_path, frames, loop=0) - optimize(gif_path) + if not skip_gifsicle: + optimize(gif_path) + \ No newline at end of file diff --git a/docs/source/theory.rst b/docs/source/theory.rst index 91fb17e29..259952e83 100644 --- a/docs/source/theory.rst +++ b/docs/source/theory.rst @@ -70,6 +70,60 @@ At first the solution will not be correct, but as the optimization algorithm ite .. note:: These animations are simplifications and do not fully capture all details of either the time-stepping or pseudo-spectral numerical optimization solution. +Time/frequency equivalence in the pseudo-spectral method +-------------------------------------------------------- + +In WecOptTool, the pseudo-spectral solution is represented using a finite set of Fourier coefficients on an equally spaced frequency grid. +This creates a direct correspondence between the frequency-domain decision variables and a periodic time-domain trajectory. +In the figure below, each harmonic component is shown individually with its amplitude and phase on the opposing frequency-domain axes, while their sum reconstructs the periodic time-domain trajectory. + +.. image:: _static/pseudospectral_equivalence.png + :width: 700 + :alt: Illustration of the relationship between frequency-domain coefficients and time-domain collocation points + :align: center + +For a chosen fundamental frequency :math:`f_1` and a maximum harmonic index :math:`n_{freq}`, the modeled frequencies are integer multiples of the fundamental frequency: + +.. math:: + f_k = k f_1 + :label: discrete_frequencies + +The pseudo-spectral representation implies a periodic time trajectory with repeat period + +.. math:: + T = \frac{1}{f_1} + :label: repeat_period + +and :math:`2 n_{freq}` time-domain collocation points. +The corresponding time step is + +.. math:: + \Delta t = \frac{1}{2 n_{freq} f_1} + :label: timestep_relation + +Thus, the choice of frequency vector determines both what dynamics can be represented in the frequency domain and how finely the corresponding periodic trajectory is sampled in time. + +Some useful interpretations are: + + * **All modeled frequencies are harmonics of** :math:`f_1`. + * **The smallest nonzero frequency** :math:`f_1` **sets the total repeat period** :math:`T`. + * **The highest modeled frequency** :math:`n_{freq} f_1` **sets the effective time resolution**. + * **The Fourier coefficients and time-domain collocation points are linked:** :math:`n_{freq}` harmonics correspond to :math:`2 n_{freq}` collocation points in time. + +In practice, this means that selecting :math:`f_1` and :math:`n_{freq}` is not only a question of frequency resolution. +It also determines the length and temporal resolution of the periodic trajectory used to enforce the dynamics and constraints. + +WaveBot example +^^^^^^^^^^^^^^^ + +The same correspondence can be seen in an actual WecOptTool solution. +The animation below shows the pseudo-spectral representation of each iteration of a +WaveBot PTO trajectory being optimized over the full periodic time window. + +.. image:: _static/wavebot_ex_xopt_iterates.gif + :width: 700 + :alt: WaveBot pseudo-spectral solution animation + :align: center Practical concerns ------------------ @@ -88,9 +142,9 @@ Automatic differentiation ^^^^^^^^^^^^^^^^^^^^^^^^^ In practice, the size of the decision vector :math:`x` from :eq:`optim_prob` will often be quite large. For a single degree of freedom device, :math:`x` can easily be :math:`\mathcal{O}(1e2)`. -To obtain high accuracy solutions to optimization problems with large numbers of decision variables, without requiring users to provide analytic gradients (i.e., the Jacobian and Hessian matrices), WecOptTool employs the `automatic differentiation`_ package `Autograd`_. -In practice, most WecOptTool users should only need to know that when writing custom functions to define their device, they should simply use the Autograd replacement for `NumPy`_ by calling :code:`import autograd.numpy as np`. -Note that Autograd does not support all of NumPy (see the `Autograd documentation`_) and using unsupported parts can result in silent failure of the automatic differentiation. +To obtain high accuracy solutions to optimization problems with large numbers of decision variables, without requiring users to provide analytic gradients (i.e., the Jacobian and Hessian matrices), WecOptTool employs the `automatic differentiation`_ package `Jax`_. +In practice, most WecOptTool users should only need to know that when writing custom functions to define their device, they should simply use the Jax replacement for `NumPy`_ by calling :code:`import jax.numpy as np`. +Note that Jax does not support all of NumPy (see the `Jax documentation`_) and using unsupported parts can result in silent failure of the automatic differentiation. Scaling ^^^^^^^ @@ -151,10 +205,10 @@ Phase Realizations Irregular waves are defined in WecOptTool as a spectrum of complex frequency-domain wave elevations. The phase of each of the elevation elements impacts the time-domain result. Thus, the standard calculation of the objective function (average power) may change across a range of phase realizations. The amount of variance in power depends on multiple factors such as the optimization problem and the frequency array. When creating an irregular wave using :py:meth:`wecopttool.waves.long_crested_wave` or :py:meth:`wecopttool.waves.irregular_wave`, :code:`nrealizations` can be used to select the number of phase realizations to be used. By default, random realizations will be used to create the selected number of wave elevation spectra. The :py:meth:`wecopttool.WEC.solve` function will automatically iterate through and solve the optimization problem for each realization, and the overall result can be found by averaging the value of the objective function across all realizations. A general recommendation is to use sufficient random phase realizations such that the total simulation time sums to 20 minutes. .. math:: - t_{total} = \frac{nrealizations}{f1} + t_{total} = \frac{n_{realizations}}{f1} :label: total_time -The selection of the number of realizations is further detailed in :doc:`_examples/tutorial_4_Pioneer`. +The selection of the number of realizations is further detailed in :cite:`Grasberger:2025ad` and :doc:`_examples/tutorial_4_Pioneer`. Troubleshooting --------------- @@ -169,7 +223,7 @@ If your simulation is not behaving as expected, consider some of the general tro * Check that absorbed power is less than or equal to the theoretical maximum .. _WEC-Sim: https://wec-sim.github.io/WEC-Sim/master/index.html -.. _Autograd: https://github.com/HIPS/autograd -.. _Autograd documentation: https://github.com/HIPS/autograd/blob/master/docs/tutorial.md#supported-and-unsupported-parts-of-numpyscipy +.. _Jax: https://github.com/jax-ml/jax +.. _Jax documentation: https://docs.jax.dev/en/latest/notebooks/Common_Gotchas_in_JAX.html .. _automatic differentiation: https://en.wikipedia.org/wiki/Automatic_differentiation .. _NumPy: https://numpy.org diff --git a/docs/source/wecopttool_refs.bib b/docs/source/wecopttool_refs.bib index e142a6ede..c46f64168 100644 --- a/docs/source/wecopttool_refs.bib +++ b/docs/source/wecopttool_refs.bib @@ -117,4 +117,18 @@ @inproceedings{Coe:2024aa month = {September}, organization = {IFAC}, title = {Co-design of a wave energy converter for autonomous power}, - year = {2024}} \ No newline at end of file + year = {2024}, + doi = {10.1016/j.ifacol.2024.10.094}} + +@article{Grasberger:2025ad, + author = {Grasberger, Jeff and Coe, Ryan and Gaebele, Daniel T. and Devin, Michael C. and Str{\"o}fer, Carlos A. Michelen and Bacelli, Giorgio}, + chapter = {ICOE 2024 special issue papers}, + doi = {10.36688/imej.8.265-270}, + journal = {International Marine Energy Journal}, + month = {October}, + number = {3}, + pages = {265-270}, + title = {Frequency array and wave phase realizations for wave energy converter control optimization}, + volume = {8}, + year = {2025}, +} \ No newline at end of file diff --git a/examples/tutorial_1_WaveBot.ipynb b/examples/tutorial_1_WaveBot.ipynb index 5b6cda6df..bea33b1f1 100644 --- a/examples/tutorial_1_WaveBot.ipynb +++ b/examples/tutorial_1_WaveBot.ipynb @@ -19,7 +19,7 @@ "\n", "We will start by loading the necessary modules: \n", "\n", - "* Import Autograd (wrapper on NumPy, required) for [automatic differentiation](https://sandialabs.github.io/WecOptTool/theory.html#automatic-differentiation)\n", + "* Import Jax (wrapper on NumPy, required) for [automatic differentiation](https://sandialabs.github.io/WecOptTool/theory.html#automatic-differentiation)\n", "* Import other packages we will use in this tutorial\n", "* Import WecOptTool " ] diff --git a/wecopttool/core.py b/wecopttool/core.py index de41daa6e..81d27b732 100644 --- a/wecopttool/core.py +++ b/wecopttool/core.py @@ -655,9 +655,9 @@ def solve(self, Optimization options passed to the optimizer. See :py:func:`scipy.optimize.minimize`. use_grad - If :python:`True`, optimization will utilize - `jax `_ - for gradients. + If :python:`True`, optimization will utilize + `jax `_ + for gradients. maximize Whether to maximize the objective function. The default is to minimize the objective function. diff --git a/wecopttool/utilities.py b/wecopttool/utilities.py index 0499288b1..152cb216e 100644 --- a/wecopttool/utilities.py +++ b/wecopttool/utilities.py @@ -274,19 +274,22 @@ def calculate_power_flows(wec, return power_flows -def plot_power_flow(power_flows: dict[str, float], +def plot_power_flow( + power_flows: dict[str, float], tolerance: Optional[float] = None, -)-> tuple(Figure, Axes): +) -> tuple[Figure, Axes]: """Plot power flow through a WEC as Sankey diagram. Parameters ---------- power_flows - Power flow dictionary produced by for example by + Power flow dictionary produced 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 are ``'Optimal Excitation'``, ``'Radiated'``, + ``'Actual Excitation'``, ``'Electrical (solver)'``, + ``'Mechanical (solver)'``, ``'Absorbed'``, + ``'Unused Potential'``, and ``'PTO Loss'``. tolerance Tolerance value for sankey diagram. """