diff --git a/examples/shapeStudy/Performance.m b/examples/shapeStudy/Performance.m new file mode 100644 index 0000000..fb71a18 --- /dev/null +++ b/examples/shapeStudy/Performance.m @@ -0,0 +1,297 @@ +classdef Performance < handle + % Data type for storage of the controlled WEC performance. + % + % This data type defines a set of parameters that are common to + % analysis of controlled WEC performance. + % + % The following parameters must be provided within the input struct + % (any additional parameters given will also be stored within the + % created object: + % + % * powPerFreq + % + % The following additional parameters are available following + % instantiation of the object: + % + % * pow + % + % Once created, the parameters in the object are read only, but the + % object can be converted to a struct, and then modified. + % + % Arguments: + % input (struct): + % A struct (not array) whose fields represent the parameters + % to be stored. + % + % Attributes: + % powPerFreq (array of float): + % The exported power of the WEC per wave frequency + % pow (float): + % The total exported power of the WEC. + % + % Methods: + % struct(): convert to struct + % + % Note: + % To create an array of Performance objects see the + % :mat:func:`+WecOptTool.types` function. + % + % -- + % + % Performance Properties: + % powPerFreq - The exported power of the WEC per wave frequency + % pow - The total exported power of the WEC + % + % Performance Methods: + % struct - convert to struct + % + % See also WecOptTool.types + % + % -- + + % Copyright 2020 National Technology & Engineering Solutions of Sandia, + % LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the + % U.S. Government retains certain rights in this software. + % + % This file is part of WecOptTool. + % + % WecOptTool is free software: you can redistribute it and/or + % modify it under the terms of the GNU General Public License as + % published by the Free Software Foundation, either version 3 of + % the License, or (at your option) any later version. + % + % WecOptTool is distributed in the hope that it will be useful, + % but WITHOUT ANY WARRANTY; without even the implied warranty of + % MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + % GNU General Public License for more details. + % + % You should have received a copy of the GNU General Public + % License along with WecOptTool. If not, see + % <https://www.gnu.org/licenses/>. + + properties + w (:,:) double {mustBeFinite,mustBeReal,mustBePositive} + ph (:,:) double {mustBeFinite,mustBeReal} + eta (:,:) double {mustBeFinite} + F0 (:,:) double {mustBeFinite} + u (:,:) double {mustBeFinite} + pos (:,:) double {mustBeFinite} + Zpto (:,:) double {} + Fpto (:,:) double {mustBeFinite} + pow (:,:) double {mustBeFinite} + name (1,:) char = 'tmp' + date (1,1) double {mustBeFinite,mustBePositive} = now + mass (:,:) double {mustBeFinite} + end + + methods + + function plotTime(obj ,t) + + if nargin < 2 + trep = obj(1).getRepeatPer(); + t = 0:0.05:trep; + end + + fig = figure('Name','Performance.plotTime'); + fig.Position = fig.Position.*[1 1 1 1.5]; + + % fields for plotting + fns = {'eta','F0','pos','u','Fpto','pow'}; + + for ii = 1:length(fns) + ax(ii) = subplot(length(fns), 1, ii); + hold on + grid on + end + + for jj = 1:length(obj) + + for ii = 1:length(fns) + timeRes.(fns{ii}) = getTimeRes(obj(jj),fns{ii}, t); + plot(ax(ii),t,timeRes.(fns{ii})) + ylabel(ax(ii),fns{ii}) + end + + for ii = 1:length(ax) - 1 + set(ax(ii),'XTickLabel',[]) + end + linkaxes(ax,'x') + xlabel(ax(end),'Time [s]') + end + xlim([t(1), t(end)]) + + if length(obj) > 1 + legend(ax(1),{obj.name}) + end + + end + + function plotFreq(obj,fig) + + if nargin < 2 || isempty(fig) + fig = figure; + end + set(fig,'Name','Performance.plotFreq'); + + fns = {'F0','u','Fpto'}; + mrks = {'o','.','+','s'}; + + n = length(obj); + for jj = 1:n + for ii = 1:length(fns) + + fv = obj(jj).(fns{ii})(:,1); % use the first column if this is PS + + % mag plot + ax(jj,1) = subplot(2,n,sub2ind([n,2],jj,1)); + title(obj(jj).name,'interpreter','none') + hold on + grid on + + stem(ax(jj,1),obj(jj).w, mag2db(abs(fv))... + ,mrks{ii},... + 'DisplayName',fns{ii},... + 'MarkerSize',8,... + 'Color','b') + + % phase plot + ax(jj,2) = subplot(2,n,sub2ind([n,2],jj,2)); + hold on + grid on + + stem(ax(jj,2),obj(jj).w, angle(fv)... + ,mrks{ii},... + 'DisplayName',fns{ii},... + 'MarkerSize',8,... + 'Color','b') + + ylim(ax(jj,2),[-pi,pi]) + end + xlabel(ax(jj,2),'Frequency [rad/s]') + end + ylabel(ax(1,1),'Magnitude [dB]') + ylabel(ax(1,2),'Angle [rad]') + legend(ax(n,1)) + linkaxes(ax,'x') + linkaxes(ax(:,1),'y') + + end + + function T = summary(obj) + + if length(obj) > 1 + for ii = 1:length(obj) + Tr(ii,:) = summary(obj(ii)); + end + + % augment names if they are the same + if any(strcmp(obj(1).name, {obj(2:end).name})) + for ii = 1:length(obj) + rnames{ii} = [obj(ii).name, '_', num2str(ii)]; + end + else + rnames = {obj.name}; + end + + Tr.Properties.RowNames = rnames; + mT = Tr; + + if nargout + T = mT; + else + disp(mT) + end + + return + + else + rnames = {obj.name}; + end + + trep = obj.getRepeatPer(); + t = linspace(0,trep,1e3); + + for jj = 1:size(obj.ph,2) % for each phase in PS cases + + tmp.pow_avg(jj) = sum(real(obj.pow(:,jj))); + + pow_t = getTimeRes(obj, 'pow', t, jj); + tmp.pow_max(jj) = max(abs(pow_t)); + + try + tmp.pow_thd(jj) = thd(pow_t); + catch ME + warning(ME.message) + tmp.pow_thd(jj) = NaN; + end + + pos_t = getTimeRes(obj, 'pos', t, jj); + tmp.pos_max(jj) = max(abs(pos_t)); + + vel_t = getTimeRes(obj, 'u', t, jj); + tmp.vel_max(jj) = max(abs(vel_t)); + + Fpto_t = getTimeRes(obj, 'Fpto', t, jj); + tmp.Fpto_max(jj) = max(abs(Fpto_t)); + end + + fn = fieldnames(tmp); + for kk = 1:length(fn) + out.(fn{kk}) = mean(tmp.(fn{kk}), 2); + end + + rnames = reshape(rnames,[],1); + + mT = table(out.pow_avg(:),out.pow_max(:),out.pow_thd(:),... + out.pos_max(:),out.vel_max(:),out.Fpto_max(:),... + 'VariableNames',... + {'AvgPow','|MaxPow|','PowTHD_dBc','MaxPos','MaxVel','MaxPTO'},... + 'RowNames',rnames); + + if nargout + T = mT; + else + disp(mT) + end + + end + + end + + methods (Access=protected) + + function [tRep] = getRepeatPer(obj) + tRep = 2*pi/(obj.w(2) - obj.w(1)); + end + + function [timeRes] = getTimeRes(obj, fn, t_vec, ph_idx) + if nargin < 4 + ph_idx = 1; + end + + if strcmp(fn,'pow') + vel = obj.getTimeRes('u',t_vec); + f = obj.getTimeRes('Fpto',t_vec); + timeRes = vel .* f; + else + timeRes = zeros(size(t_vec)); + fv = obj.(fn)(:,ph_idx); % use the first column if this is PS + for ii = 1:length(obj.w) % for each freq. TODO - use IFFT + timeRes = timeRes ... + + real(fv(ii) * exp(1i * obj.w(ii) * t_vec)); + end + end + end + +% function checkSizes(varargin) % TODO +% n = length(varargin); +% for ii = 1:n +% if ~isequal(varargin(varargin{ii}),size(varargin{1})) +% error('Frequency vectors must have same size') +% end +% end +% end + + end +end \ No newline at end of file diff --git a/examples/shapeStudy/ShapeStudy.m b/examples/shapeStudy/ShapeStudy.m new file mode 100644 index 0000000..cb79dcf --- /dev/null +++ b/examples/shapeStudy/ShapeStudy.m @@ -0,0 +1,475 @@ +% ShapeStudy +% +% This file is part of WecOptTool. +% +% WecOptTool is free software: you can redistribute it and/or modify it +% under the terms of the GNU General Public License as published by the +% Free Software Foundation, either version 3 of the License, or (at +% your option) any later version. +% +% WecOptTool is distributed in the hope that it will be useful, but +% WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +% General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with WecOptTool. If not, see <https://www.gnu.org/licenses/>. +constant='Spect'; +%constant='Force'; +%constant='Power'; + +%% define sea state of interest +dw = 0.3142; +nf = 50; +%nf = 30; +w = dw * (1:nf)'; +S = jonswap(w,[0.25, 3.5, 1]); +SS = WecOptTool.SeaState(S); + +% Amp = 0.125/2; +% wp = w(6); +% fp = wp/(2*pi); +% Tp = 1/fp; +% SS = WecOptTool.SeaState.regularWave(w,[Amp,Tp]); + +%% set up device types + +controlType{1} = 'CC'; +controlType{2} = 'P'; +controlType{3} = 'PS'; +zmax = 0.6; +fmax = 1e10; + +folder = WecOptTool.AutoFolder(); + +%% Create Devices & Plot Geometries + +%AR = [ 0.1, 0.125, 0.15, 0.2, 0.3, 0.4, 0.5, 0.7, 1.0, 2, 3, 6, 10]; +AR = [0.3, 0.4, 0.5, 0.7, 1.0, 2, 3, 6, 10]; +%AR = [ 5, 10, 50, 100, 200]; +%AR = [0.1:0.5:10]; +volume=0.875; +heights = (volume./(pi * AR.^2)).^(1/3); +radii = AR.*heights; + +fig = figure('name','Shape Study'); +%fig.Position = fig.Position .*[1,1,1.5,0.75]; +hold on +grid on +set(gca, 'fontsize',16); +for ii = 1:length(AR) + radius = radii(ii); + height = heights(ii); + xCoords = [0, radius, radius, 0]; + yCoords = [0.2, 0.2, -height, -height]; + ax = gca; + p(ii) = plot(ax, xCoords, yCoords, 'bo-','DisplayName',num2str(ii)); +end +axis equal +% xlabel('Radius [m]') +% ylabel('Height [m]') +% ylim([-inf 0]) +% x = [0.7 0.5]; +% y = [0.2 0.2]; +% annotation('textarrow',x,y,'String','Low AR') +% x = [0.85 0.85]; +% y = [0.4 0.7]; +% annotation('textarrow',x,y,'String','High AR') +% saveas(fig, 'plots/aspectRatios.pdf') + +%% Running hydrodynamics + +N = length(AR); +% Determine the value of the constant force +iAR = N; +remaingARIndicies = setdiff(find(AR), iAR); + +[deviceHydro, mesh] = designDevice('parametric', folder.path, ... + radii(iAR), heights(iAR), w); +% Tune mass to desired natural frequency +wdes = 0.625*2*pi; +dynModel = getDynamicsModel(deviceHydro, ... + SS, ... + 'linear', ... + wdes); +%mean(dynModel.F0) +% Constant Force? +if all(constant == 'Force') + Fconstant = ones(length(w),1) * mean(dynModel.F0); + dynModel.F0 = Fconstant; +elseif all(constant == 'Power') + Fconstant = sqrt(8*real([dynModel.Zi])); + dynModel.F0 = Fconstant(:,1); +end +if ~all(constant == 'Spect') + dynModel.eta_fd = dynModel.F0 ./ dynModel.Hex; +end +perform = simulateDevice(dynModel(1), controlType{1}, ... + 'interpMethod','nearest', ... + 'Zmax',zmax, 'Fmax',fmax); + +deviceHydro = repmat(deviceHydro, N, 1 ); +meshes = repmat(mesh, N, 1 ); +dynModel = repmat(dynModel, N, 1 ); +perform = repmat(perform, N, 1 ); + +for i = remaingARIndicies + radius = radii(i); + height = heights(i); + [deviceHydro(i), meshes(i)] = designDevice('parametric', ... + folder.path, radius, height, w); + + %% Tune Device Mass to a Natural Frequency + dynModel(i) = getDynamicsModel(deviceHydro(i), ... + SS, ... + 'linear', ... + wdes); + + if all(constant == 'Force') + Fconstant = ones(length(w),1) * mean(dynModel(iAR).F0);%Global Const + %Fconstant = ones(length(w),1) * mean(dynModel(i).F0); % AR const + dynModel(i).F0 = Fconstant; + elseif all(constant == 'Power') + Fconstant = sqrt(8*real([dynModel(i).Zi])); + dynModel(i).F0 = Fconstant(:,1); + end + if ~all(constant == 'Spect') + dynModel(i).eta_fd = dynModel(i).F0 ./ dynModel(i).Hex; + end + perform(i) = simulateDevice(dynModel(i), controlType{1}, ... + 'interpMethod','nearest', ... + 'Zmax',zmax, 'Fmax',fmax); +end + +%% Plot the parametric meshes +plotMesh=false; +if plotMesh == true +%WecOptTool.plot.plotMesh(mesh) +for ii = 1:length(AR) + WecOptTool.plot.plotMesh(meshes(ii)) +end +end + + +%% Plot mass ratio +fig = figure('name','Mass Ratio'); +hold on +grid on +set(gca, 'fontsize',16); +semilogx(AR,[dynModel.mass] ./( [deviceHydro.Vo] .* [deviceHydro.rho] ), ... + '--xk' ) +set(gca,'XScale','log') +%ylim([0 1]) +xlabel('Aspect Ratio [-]') +ylabel('Mass ratio, m^\prime/m [-]') +saveas(fig, 'plots/massRatios.pdf') + + +%% Plot hydro +%close all +clear ax + +[xp,yp] = meshgrid(w,AR); + +flag3D=false; +%flag3D=true; + +% Figure 1: Added Mass +nFig = 1; +fh(nFig) = figure('name','Radiation added mass (A)'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on +grid on +A = [dynModel.A]-[dynModel.Ainf]; +if flag3D == 1 + surf(xp',yp',(A)); + rotate3d on +else + contourf(xp',yp',(A)); + c(nFig) = colorbar; +end + + +% Figure 2: Radiation Damping +nFig = nFig + 1; +fh(nFig) = figure('name','Radiation wave damping (B)'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on +grid on +if flag3D == 1 + surf(xp',yp',[dynModel.B]) + rotate3d on +else + contourf(xp',yp',[dynModel.B]) + c(nFig) = colorbar; +end + + +% Figure 3: Position +nFig = nFig + 1; +fh(nFig) = figure('name','|Position|'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on +grid on + +if flag3D == 1 + surf(xp',yp', abs([perform.pos]) ) + rotate3d on +else + contourf(xp',yp', abs([perform.pos]))% ./max(max(abs([perform.pos])))) + c(nFig) = colorbar; + set(gca,'ColorScale','log') +end + + +% Figure 4: Power +nFig = nFig + 1; +fh(nFig) = figure('name','|\Re(Power)|'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on +grid on +% surf(xp',yp',abs(Ex)./max(max(abs(Ex)))) +Popt = abs([dynModel.Hex].*SS.S).^2./(8*real([dynModel.Zi])); +Popt(~isfinite(Popt)) = 0; +%surf(xp',yp',Popt./max(max(Popt))) + +if flag3D == 1 + surf(xp',yp',abs(real([perform.pow])) ) + rotate3d on +else + contourf(xp',yp',abs(real([perform.pow])) ) + c(nFig) = colorbar; + set(gca,'ColorScale','log') +end + + +% Figure 5: Zpto +nFig = nFig + 1; +fh(nFig) = figure('name','|Zpto|'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on +grid on +%surf(xp',yp',abs([perform.Zpto])./max(max(abs([perform.Zpto])))) +if flag3D == 1 + surf(xp',yp',abs([perform.Zpto])) + rotate3d on +else + contourf(xp',yp',abs([perform.Zpto])) + c(nFig) = colorbar; + set(gca,'ColorScale','log') +end + + +% Figure 6: Real Zpto +nFig = nFig + 1; +fh(nFig) = figure('name','Real(Zpto)'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on +grid on +if flag3D == 1 + surf(xp',yp',real([perform.Zpto]) ) + rotate3d on +else + contourf(xp',yp',real([perform.Zpto]) ) + c(nFig) = colorbar; +end + + +% Figure 7: Imag Zpto +nFig = nFig + 1; +fh(nFig) = figure('name','Im(Zpto)'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on +grid on + +if flag3D == 1 + surf(xp',yp',imag([perform.Zpto]) ) + surf(xp',yp',zeros(size(xp')),'FaceAlpha',0.25,'EdgeColor','none',... + 'FaceColor','blue') + rotate3d on +else + contourf(xp',yp',imag([perform.Zpto]) ) + c(nFig) = colorbar; +end + +% Figure 8: Velocity +nFig = nFig + 1; +fh(nFig) = figure('name','|Velocity|'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on +grid on + +if flag3D == 1 + surf(xp',yp',abs([perform.u]) ) + rotate3d on +else + contourf(xp',yp',abs([perform.u]) ) + c(nFig) = colorbar; + set(gca,'ColorScale','log') +end + + +% Figure 9: eta_fd +nFig = nFig + 1; +fh(nFig) = figure('name','|\eta_{fd}|'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on +grid on +eta = [dynModel.eta_fd]; +if flag3D == 1 + surf(xp(:,:)',yp(:,:)',abs(eta(:,:) ) )% ./ max(max((abs([eta]))))) + set(ax(nFig),'zscale','log') + rotate3d on +else + contourf(xp(:,:)',yp(:,:)',abs(eta(:,:) ) )% ./ max(max((abs([eta]))))) + c(nFig) = colorbar; + set(gca,'ColorScale','log') +end + + +% Figure 10: Fex +nFig = nFig + 1; +fh(nFig) = figure('name','|F_{Ex}|'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on + +if flag3D == 1 + surf(xp',yp',abs([perform.F0])) + set(ax(nFig),'zscale','log') + rotate3d on +else + contourf(xp(:,:)',yp(:,:)',abs([perform.F0]) ) + c(nFig) = colorbar; + set(gca,'ColorScale','log') +end + + +% Figure 11: Zi +nFig = nFig + 1; +fh(nFig) = figure('name','|Z_{i}|'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on + +if flag3D == 1 + surf(xp',yp',abs([dynModel.Zi])) + set(ax(nFig),'zscale','log') + rotate3d on +else + contourf(xp(:,:)',yp(:,:)',abs([dynModel.Zi]) ) + c(nFig) = colorbar; + set(gca,'ColorScale','log') +end + + +% Figure 12: Hex +nFig = nFig + 1; +fh(nFig) = figure('name','H_{ex}'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on + +if flag3D == 1 + surf(xp',yp',abs([dynModel.Hex])) + set(ax(nFig),'zscale','log') + rotate3d on +else + contourf(xp(:,:)',yp(:,:)',abs([dynModel.Hex]) ) + c(nFig) = colorbar; + set(gca,'ColorScale','log') +end + +% Figure 13: Reactive Power +nFig = nFig + 1; +fh(nFig) = figure('name','|\Im(Power)|'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on + +if flag3D == 1 + surf(xp',yp',imag([perform.pow])) + rotate3d on +else + contourf(xp',yp',abs(imag([perform.pow]))) + c(nFig) = colorbar; + set(gca,'ColorScale','log') +end + + +if flag3D == 1 + Link = linkprop(ax, ... + {'CameraUpVector', 'CameraPosition', 'CameraTarget'}); + setappdata(gcf, 'StoreTheLink', Link); + view([20, 12]) +end + + +for ii = 1:nFig + xlabel(ax(ii),'Freq. [rad/s]') + ylabel(ax(ii),'Aspect ratio [ ]') + if flag3D==1 + zlabel(ax(ii),fh(ii).Name) + else + c(ii).Label.String = fh(ii).Name; + end + %fname = strrep(strrep(strrep(strrep(fh(ii).Name,'|',''), '\',''), '{',''),'}',''); + fname = strrep(fh(ii).Name,'|',''); + path = "plots/%s/%s.pdf"; + + str = sprintf(path,constant,fname); + saveas(fh(ii), str) +end + + +%% Plot omega, Zi for each AR (2D) +nFig = nFig + 1; +fh(nFig) = figure('name','Z_{i}'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +hold on +grid +for ii = 1:length(AR) + plot(xp(ii,:)',abs([dynModel(ii).Zi]), 'DisplayName',string(AR(ii))) +end +xlabel('Freq. [rad/s]') +ylabel('Z_i [\Omega]') +legend() + + +%% Plot omega, Zi for each AR (2D) +nFig = nFig + 1; +fh(nFig) = figure('name','Z_{i}'); +ax(nFig) = gca; +set(gca, 'fontsize',16); +set(gca, 'yscale','log'); +hold on +grid +for ii = 1:length(AR) + plot(w',abs(imag([dynModel(ii).Zi])), 'DisplayName',string(AR(ii))) +end +xlabel('Freq. [rad/s]') +ylabel('') +legend() + +%% Bode +hold on + +for ii = 1:length(AR) + bode(frd(dynModel(ii).Zi,w)), +end +xlabel('Freq. [rad/s]') +xlim([0.3,11]) +grid +legendCell = cellstr(num2str(AR')); +legend(legendCell) + diff --git a/examples/shapeStudy/designDevice.m b/examples/shapeStudy/designDevice.m new file mode 100644 index 0000000..ed80e68 --- /dev/null +++ b/examples/shapeStudy/designDevice.m @@ -0,0 +1,98 @@ +function [hydro, meshes] = designDevice(type, varargin) + % WaveBot WEC based on the Sandia "WaveBot" device. + % + % The WaveBot is a model-scale wave energy converter (WEC) tested in + % the Navy's Manuevering and Sea Keeping (MASK) basin. Reports and + % papers about the WaveBot are available at advweccntrls.sandia.gov. + + switch type + + case 'existing' + hydro = WecOptTool.geometry.existingNEMOH(varargin{:}); + case 'scalar' + hydro = getHydroScalar(varargin{:}); + case 'parametric' + [hydro, meshes] = getHydroParametric(varargin{:}); + otherwise + error('WecOptTool:UnknownGeometryType',... + 'Invalid geometry type') + end + +end + + +function [hydro, meshes] = getHydroParametric(folder, r1, d1, w) + + if w(1) == 0 + w = w(2:end); + end + + z0 = 0; + r = [0, r1, r1, r1, 0]; + z = [z0, z0, (-d1-z0)/2, -d1, -d1]; + + % Mesh + ntheta = 20; + nfobj = 200; + zG = 0; + + meshes = WecOptTool.mesh("AxiMesh", ... + folder, ... + r, ... + z, ... + ntheta, ... + nfobj, ... + zG, ... + 1); + + hydro = WecOptTool.solver("NEMOH", folder, meshes, w); + +end + +% function hydro = getHydroScalar(folder, lambda, w) +% +% if w(1) == 0 +% error('WecOptTool:UnknownGeometryType',... +% 'Invalid frequency vector') % TODO - more checks +% end +% +% r = lambda * [0, 0.88, 0.88, 0.35, 0]; +% z = lambda * [0.2, 0.2, -0.16, -0.53, -0.53]; +% +% % Mesh +% ntheta = 20; +% nfobj = 200; +% zG = 0; +% +% meshes = WecOptTool.mesh("AxiMesh", ... +% folder, ... +% r, ... +% z, ... +% ntheta, ... +% nfobj, ... +% zG, ... +% 1); +% +% hydro = WecOptTool.solver("NEMOH", folder, meshes, w); +% +% end + + +% Copyright 2020 National Technology & Engineering Solutions of Sandia, +% LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the +% U.S. Government retains certain rights in this software. +% +% This file is part of WecOptTool. +% +% WecOptTool is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% WecOptTool is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with WecOptTool. If not, see <https://www.gnu.org/licenses/>. diff --git a/examples/shapeStudy/getDynamicsModel.m b/examples/shapeStudy/getDynamicsModel.m new file mode 100644 index 0000000..e095640 --- /dev/null +++ b/examples/shapeStudy/getDynamicsModel.m @@ -0,0 +1,89 @@ +function dynModel = getDynamicsModel(hydro, SS, interpMethod, wdes) + + % Restoring + K = hydro.C(3,3) * hydro.g * hydro.rho; + + function result = interp_mass(hydro, dof1, dof2, w) + result = interp1(hydro.w, ... + squeeze(hydro.A(dof1, dof2, :)), ... + w, ... + interpMethod, ... + 0); + end + + function result = interp_rad(hydro, dof1, dof2, w) + result = interp1(hydro.w, ... + squeeze(hydro.B(dof1, dof2, :)), ... + w, ... + interpMethod, ... + 0); + end + + function result = interp_ex(hydro, dof, w) + + h = squeeze(hydro.ex(dof, 1, :)); + result = interp1(hydro.w, h ,w, interpMethod, 0); + + end + + w = hydro.w(:); + dw = w(2) - w(1); + + % Calculate wave amplitude + waveAmpSS = SS.getAmplitudeSpectrum; + waveAmp = interp1(SS.w, waveAmpSS, w, interpMethod, 'extrap'); + + % Row vector of random phases + ph = rand(size(waveAmp))*2*pi; + + % Wave height in frequency domain + eta_fd = waveAmp .* exp(1i * ph); + eta_fd = eta_fd(:); + + % radiation damping FRF + B = interp_rad(hydro, 3, 3, w) * hydro.rho .* w; +% B(:,ii) = squeeze(hydro.B(3,3,:)).*w'*rho; + + % added mass FRF + A = interp_mass(hydro, 3, 3, w) * hydro.rho; + Ainf = hydro.Ainf(3,3)*hydro.rho; + + % friction + Bf = max(B) * 0.1; % TODO - make this adjustable + + % Tune device mass to desired natural frequency + m = hydro.Vo * hydro.rho; + fun = @(m) tune_wdes(wdes,m,K,w',A); + mass = fminsearch(fun,m); + + % intrinsic impedance + Zi = B + Bf + 1i * (w .* (mass + A) - K ./ w); + + % Excitation Forces + Hex = interp_ex(hydro, 3, w) * hydro.g * hydro.rho; + F0 = Hex .* eta_fd; + + dynModel.mass = mass; + dynModel.K = K; + dynModel.w = w; + dynModel.eta_fd = eta_fd; + dynModel.dw = dw; + dynModel.wave_amp = waveAmp; + dynModel.ph = ph; + dynModel.B = B; + dynModel.A = A; + dynModel.Ainf = Ainf; + dynModel.Bf = Bf; + dynModel.Zi = Zi; + dynModel.Hex = Hex; + dynModel.F0 = F0; + + +end + +function [err] = tune_wdes(wdes,m,k,w,A) + fun1 = @(w1) w1.^2.*(m + interp1(w,A,w1)) - k; + options = optimset('Display','off'); + w0 = fsolve(fun1,5, options); + err = (w0 - wdes).^2; +end \ No newline at end of file diff --git a/examples/shapeStudy/simulateDevice.m b/examples/shapeStudy/simulateDevice.m new file mode 100644 index 0000000..2361baa --- /dev/null +++ b/examples/shapeStudy/simulateDevice.m @@ -0,0 +1,400 @@ +function performance = simulateDevice(dynModel, controlType,... + options) + % simulateDevice WEC based on the Sandia "WaveBot" device. + % + % The WaveBot is a model-scale wave energy converter (WEC) tested in + % the Navy's Manuevering and Sea Keeping (MASK) basin. Reports and + % papers about the WaveBot are available at advweccntrls.sandia.gov. + % + % Arguments: + % dynModel + % controlType controller type: + % complex conjugate: 'CC' + % proportional damping: 'P' + % pseudo-spectral: 'PS' + % name-value pairs + % interpMethod (optional) method to use for linear interpolation + % Zmax (only valid for controlType == 'controlType') maximum + % displacement + % Fmax (only valid for controlType == 'controlType') maximum + % PTO force + % + % See also WecOptTool.SeaState, interp1 + %hydro (1,1) WecOptTool.Hydrodynamics + arguments + dynModel + controlType (1,1) string + options.Zmax (1,:) double = Inf % TODO - can be assymetric, need to check throughout + options.Fmax (1,:) double = Inf + options.interpMethod (1,1) string = 'linear' + end + + + switch controlType + case 'CC' + performance = complexCongugateControl(dynModel); + case 'P' + performance = dampingControl(dynModel); + case 'PS' + performance = psControl(dynModel,options.Zmax, options.Fmax); + end +end + + + +function myPerf = complexCongugateControl(dynModel,~) + + myPerf = Performance(); + + myPerf.Zpto = conj(dynModel.Zi); + + % velocity + myPerf.u = dynModel.F0 ./ (myPerf.Zpto + dynModel.Zi); + + % position + myPerf.pos = myPerf.u ./ (1i * dynModel.w); + + % PTO force + myPerf.Fpto = -1 * myPerf.Zpto .* myPerf.u; + + % power + myPerf.pow = 0.5 * myPerf.Fpto .* conj(myPerf.u); + + myPerf.ph = dynModel.ph; + myPerf.w = dynModel.w; + myPerf.eta = dynModel.eta_fd; + myPerf.F0 = dynModel.F0; + myPerf.mass = dynModel.mass; + +end + +function myPerf = dampingControl(dynModel,~) + + myPerf = Performance(); + + P_max = @(b) -0.5*b*sum(abs(dynModel.F0 ./ ... + (dynModel.Zi + b)).^2); + + % Solve for damping to produce most power (can do analytically for a + % single frequency, but must use numerical solution for spectrum). Note + % that fval is the sum of power absorbed (negative being "good") - the + % following should be true: -1 * fval = sum(pow), where pow is the + % frequency dependent array calculated below. + [B_opt, ~] = fminsearch(P_max, max(real(dynModel.Zi))); + + % PTO impedance + myPerf.Zpto = complex(B_opt * ones(size(dynModel.Zi)),0); + + % velocity + myPerf.u = dynModel.F0 ./ (myPerf.Zpto + dynModel.Zi); + + % position + myPerf.pos = myPerf.u ./ (1i * dynModel.w); + + % PTO force + myPerf.Fpto = -1 * myPerf.Zpto .* myPerf.u; + + % power + myPerf.pow = 0.5 * myPerf.Fpto .* conj(myPerf.u); + + + myPerf.ph = dynModel.ph; + myPerf.w = dynModel.w; + myPerf.eta = dynModel.eta_fd; + myPerf.F0 = dynModel.F0; + myPerf.mass = dynModel.mass; + +end + +function myPerf = psControl(dynModel,delta_Zmax,delta_Fmax) +% motion = getPSCoefficients(motion, delta_Zmax, delta_Fmax); +% ps.wave_amp = waveAmp; % TODO +% +% % Use mutliple phase realizations for PS at the model +% % is nonlinear (note that we use the original phasing +% % from the other cases) +% n_ph = 5; +% ph_mat = [ph, rand(length(ps.w), n_ph-1)]; +% +% n_freqs = length(motion.w); +% phasePowMat = zeros(n_ph, 1); +% powPerFreqMat = zeros(n_freqs, n_ph); +% +% for ind_ph = 1 : n_ph +% +% ph = ph_mat(:, ind_ph); +% % [powTot, fRes(ind_ph), tRes(ind_ph)] = getPSPhasePower(ps, ph); +% [pow, powPerFreq] = getPSPhasePower(motion, ph) +% phasePowMat(ind_ph) = powTot; +% powPerFreqMat(:, ind_ph) = fRes(ind_ph).pow; +% +% end +% +% ph = ph_mat(:,1); +% u = fRes(1).vel; +% pos = fRes(1).pos; +% Zpto = nan(size(motion.hydro.Zi)); % TODO +% Fpto = fRes(1).u; +% pow = powPerFreqMat(:,1); + + arguments + dynModel + delta_Zmax (1,:) double {mustBeFinite,mustBeReal,mustBePositive} + delta_Fmax (1,:) double {mustBeFinite,mustBeReal,mustBePositive} + end + + % Fix random seed <- Do we want this??? + rng(1); + + % Reformulate equations of motion + dynModel = getPSCoefficients(dynModel, delta_Zmax, delta_Fmax); + + % Add phase realizations + n_ph = 5; + ph_mat = [dynModel.ph, rand(length(dynModel.w), n_ph-1)]; + + for ind_ph = 1 : n_ph + + ph = ph_mat(:, ind_ph); + [phasePowMat(ind_ph), fRes(ind_ph), tRes(ind_ph)] = ... + getPSPhasePower(dynModel, ph); + + + pos(:, ind_ph) = fRes(ind_ph).pos; + u(:, ind_ph) = fRes(ind_ph).vel; + Zpto(:, ind_ph) = fRes(ind_ph).Zpto; + Fpto(:, ind_ph) = fRes(ind_ph).u; + pow(:, ind_ph) = fRes(ind_ph).pow; + eta(:, ind_ph) = fRes(ind_ph).eta; + F0(:, ind_ph) = fRes(ind_ph).F0; + + end + + % assemble results + myPerf = Performance(); + myPerf.w = dynModel.w; + myPerf.eta = eta; + myPerf.F0 = F0; + myPerf.ph = ph_mat; + myPerf.u = u; + myPerf.pos = pos; + myPerf.Zpto = Zpto; + myPerf.Fpto = Fpto; + myPerf.pow = pow; + myPerf.mass = dynModel.mass; + +end + +function dynModel = getPSCoefficients(dynModel, delta_Zmax, delta_Fmax) + % getPSCoefficients constructs the necessary coefficients and + % matrices used in the pseudospectral control optimization + % problem + % + % Note that these coefficients are not sea state dependent, + % thus it is beneficial to find them once only when doing a + % study involving multiple sea states. + % + % Bacelli 2014: Background Chapter 4.1, 4.2; RM3 in section 6.1 + + % Number of frequency - half the number of Fourier coefficients + Nf = length(dynModel.w); + + % Collocation points uniformly distributed between 0 and T + % note that we have 2*Nf collocation points since we will have + % two Fourier coefficients for each frequency + Nc = (2*Nf) + 2; + + % Rebuild frequency vector to ensure monotonically increasing + % with w(1) = w0 + w0 = dynModel.dw; % fundamental frequency + T = 2 * pi/w0; % '' period + + % Building cost function component + % we will form the cost function as transpose(x) * H x, where x + % is a vector of [vel, u]; we want the product above to result + % in power (u*vel) + H = [0,1;1,0]; + H_mat = 0.5 * kron(H, eye(2*Nf)); + + % Building matrices B33 and A33 + Adiag33 = zeros(2*Nf-1,1); + Bdiag33 = zeros(2*Nf,1); + + Adiag33(1:2:end) = dynModel.w.* dynModel.A; + Bdiag33(1:2:end) = dynModel.B; + Bdiag33(2:2:end) = Bdiag33(1:2:end); + + Bmat = diag(Bdiag33); + Amat = diag(Adiag33,1); + Amat = Amat - Amat'; + + G = Amat + Bmat; + + B = dynModel.Bf * eye(2*Nf); + C = blkdiag(dynModel.K * eye(2*Nf)); + M = blkdiag(dynModel.mass * eye(2*Nf)); + + % Building derivative matrix + d = [dynModel.w(:)'; zeros(1, length(dynModel.w))]; + Dphi1 = diag(d(1:end-1), 1); + Dphi1 = (Dphi1 - Dphi1'); + Dphi = blkdiag(Dphi1); + + % scaling factor to improve optimization performance + m_scale = dynModel.mass; + + % equality constraints for EOM + P = (M*Dphi + B + G + (C / Dphi)) / m_scale; + Aeq = [P, -eye(2*Nf) ]; + Aeq = [Aeq, zeros(2*Nf,2); + zeros(1,4*Nf), dynModel.K / m_scale, -1]; + + % Calculating collocation points for constraints + tkp = linspace(0, T, 4*(Nc)); + tkp = tkp(1:end); + Wtkp = dynModel.w*tkp; + Phip1 = zeros(2*size(Wtkp,1),size(Wtkp,2)); + Phip1(1:2:end,:) = cos(Wtkp); + Phip1(2:2:end,:) = sin(Wtkp); + + Phip = blkdiag(Phip1); + + A_ineq = [kron([1 0], Phip1' / Dphi1), ones(4*Nc,1), zeros(4*Nc,1)]; + A_ineq = [A_ineq; -A_ineq]; + + % position constraints + if length(delta_Zmax)==1 + B_ineq = [ones(size(A_ineq, 1),1) * delta_Zmax]; + else + B_ineq = [ones(size(A_ineq, 1)/2,1) * max(delta_Zmax); + -ones(size(A_ineq, 1)/2,1) * min(delta_Zmax)]; + end + + % force constraints + siz = size(A_ineq); + forc = [kron([0 1], Phip'), zeros(4*Nc,1), ones(4*Nc,1)]; + if length(delta_Fmax)==1 + B_ineq = [B_ineq; ones(siz(1),1) * delta_Fmax/m_scale]; + else + B_ineq = [B_ineq; ones(siz(1)/2,1) * max(delta_Fmax)/m_scale; + -ones(siz(1)/2,1) * min(delta_Fmax)/m_scale]; + end + A_ineq = [A_ineq; forc; -forc]; + + dynModel.Nf = Nf; + dynModel.T = T; + dynModel.H_mat = H_mat; + dynModel.tkp = tkp; + dynModel.Aeq = Aeq; + dynModel.A_ineq = A_ineq; + dynModel.B_ineq = B_ineq; + dynModel.Phip = Phip; + dynModel.Phip1 = Phip1; + dynModel.Dphi = Dphi; + dynModel.mass_scale = m_scale; +end + +function [powTot, fRes, tRes] = getPSPhasePower(dynModel, ph) + % getPSPhasePower calculates power using the pseudospectral + % method given a phase and a descrption of the body movement. + % Returns total phase power and power per frequency + + eta_fd = dynModel.wave_amp .* exp(1i*ph); + E3 = dynModel.Hex .* eta_fd; + + fef3 = zeros(2*dynModel.Nf,1); + + fef3(1:2:end) = real(E3); + fef3(2:2:end) = -imag(E3); + + Beq = [fef3; 0] / dynModel.mass_scale; + + % constrained optimization settings + qp_options = optimoptions('fmincon', ... + 'Algorithm', 'sqp', ... + 'Display', 'off', ... + 'MaxIterations', 1e3, ... + 'MaxFunctionEvaluations', 1e5, ... + 'OptimalityTolerance', 1e-8, ... + 'StepTolerance', 1e-8); + + siz = size(dynModel.A_ineq); + X0 = zeros(siz(2),1); + [y, fval, exitflag, output] = fmincon(@pow_calc,... + X0,... + dynModel.A_ineq,... + dynModel.B_ineq,... + dynModel.Aeq,... % Aeq and Beq are the hydrodynamic model + Beq,... + [], [], [],... + qp_options); + + % if exitflag ~= 1 % for debugging + % disp(exitflag) + % disp(output) + % end + + % y is a column vector containing [vel; u] of the + % pseudospectral coefficients + tmp = reshape(y(1:end-2),[],2); + x1hat = tmp(:,1); + uhat = tmp(:,2); + + % find the spectra + ps2spec = @(x) (x(1:2:end) - 1i * x(2:2:end)); % TODO - probably make this a global function + velFreq = ps2spec(x1hat); + posFreq = velFreq ./ (1i * dynModel.w); + uFreq = dynModel.mass_scale * ps2spec(uhat); + powFreq = 1/2 * uFreq .* conj(velFreq); + zFreq = uFreq ./ velFreq; + + % find time histories + spec2time = @(x) dynModel.Phip' * x; % TODO - probably make this a global function + velT = spec2time(x1hat); + posT = y(end-1) + (dynModel.Phip' / dynModel.Dphi) * x1hat; + uT = dynModel.mass_scale * (y(end) + spec2time(uhat)); + powT = 1 * velT .* uT; + + powTot = trapz(dynModel.tkp, powT) / (dynModel.tkp(end) - dynModel.tkp(1)); + assert(WecOptTool.math.isClose(powTot, sum(real(powFreq)),... + 'rtol', eps*1e2),... + sprintf('Mismatch in PS results\n\tpowTot: %.3e\n\tpowFreq: %.3e',... + powTot,sum(real(powFreq)))) + + % assemble outputs + fRes.pos = posFreq; + fRes.vel = velFreq; + fRes.u = uFreq; + fRes.pow = powFreq; + fRes.Zpto = zFreq; + fRes.eta = eta_fd; + fRes.F0 = E3; + + tRes.pos = posT; + tRes.vel = velT; + tRes.u = uT; + tRes.pow = powT; + + function P = pow_calc(X) + P = X(1:end-2)' * dynModel.H_mat * X(1:end-2); % 1/2 factor dropped for simplicity + end +end + +% Copyright 2020 National Technology & Engineering Solutions of Sandia, +% LLC (NTESS). Under the terms of Contract DE-NA0003525 with NTESS, the +% U.S. Government retains certain rights in this software. +% +% This file is part of WecOptTool. +% +% WecOptTool is free software: you can redistribute it and/or modify +% it under the terms of the GNU General Public License as published by +% the Free Software Foundation, either version 3 of the License, or +% (at your option) any later version. +% +% WecOptTool is distributed in the hope that it will be useful, +% but WITHOUT ANY WARRANTY; without even the implied warranty of +% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +% GNU General Public License for more details. +% +% You should have received a copy of the GNU General Public License +% along with WecOptTool. If not, see <https://www.gnu.org/licenses/>. diff --git a/toolbox/+WecOptTool/SeaState.m b/toolbox/+WecOptTool/SeaState.m index cc85979..52702ea 100644 --- a/toolbox/+WecOptTool/SeaState.m +++ b/toolbox/+WecOptTool/SeaState.m @@ -418,7 +418,7 @@ function checkSpectrum(S) % >>> Hm0 = 5; % >>> Tp = 8; % >>> S = bretschneider([],[Hm0,Tp]); - % >>> WecOptTool.utils.checkSpectrum(S) + % >>> WecOptTool.SeaState.checkSpectrum(S) % arguments