From af5459537b25c15bab8a284676422cd41091a554 Mon Sep 17 00:00:00 2001
From: Rakotondratsimba Lyraie <liz_tp@proton.me>
Date: Tue, 19 Sep 2023 14:08:07 +0000
Subject: [PATCH] feat: add SMST coaxial model

- Adapt upstream velocity definition
- Proper calculation of inflow velocity for second rotor
- Modify Results class to store multiple rotors
- Adapt BEMT indvel
---
 src/classes/@Blade/Blade.m                |  2 +
 src/classes/@ElemPerf/ElemPerf.m          | 12 +++-
 src/classes/@ElemPerf/calcforces.m        |  2 +-
 src/classes/@ElemPerf/updateupstreamvel.m | 85 +++++++++++++++++++++++
 src/classes/@OperRotor/OperRotor.m        |  6 ++
 src/classes/@Result/Result.m              | 23 +++---
 src/classes/@Result/filter.m              |  4 +-
 src/configs/caradonna1981.m               |  1 +
 src/configs/knight1937.m                  |  1 +
 src/configs/template.m                    |  1 +
 src/configs/templatecoax.m                |  1 +
 src/rotare.m                              |  2 +-
 src/solvers/bemt.m                        |  8 +--
 src/solvers/indvel.m                      |  2 +
 14 files changed, 131 insertions(+), 19 deletions(-)
 create mode 100644 src/classes/@ElemPerf/updateupstreamvel.m

diff --git a/src/classes/@Blade/Blade.m b/src/classes/@Blade/Blade.m
index 19c5462..e8509d7 100644
--- a/src/classes/@Blade/Blade.m
+++ b/src/classes/@Blade/Blade.m
@@ -44,6 +44,7 @@ classdef Blade < handle
     %   twist   : Twist of the guide stations, [deg] (same size as rad)
     %   iAf     : Index of the airfoil for the guide stations, [-] (same size as rad)
     %   nElem   : Number of elements to mesh the whole blade, [-]
+    %   spinDir : Rotational direction of the current rotor, [-]
     %
     % See also: Rotor, rotare, template, af_tools.Airfoil.
     %
@@ -69,6 +70,7 @@ classdef Blade < handle
         chord (1, :) double {mustBePositive} % Element chord, [m]
         twist (1, :) double {mustBeFinite}   % Element twist (also called stagger angle), [rad]
         iAf (1, :) double {mustBePositive}   % Index of airfoil to use for each element, [-]
+        spinDir (1, 1) % Rotational direction of the current rotor, [-]
     end
 
     properties (SetAccess = private, Dependent)
diff --git a/src/classes/@ElemPerf/ElemPerf.m b/src/classes/@ElemPerf/ElemPerf.m
index e30aafb..68a75e7 100644
--- a/src/classes/@ElemPerf/ElemPerf.m
+++ b/src/classes/@ElemPerf/ElemPerf.m
@@ -85,6 +85,7 @@ classdef ElemPerf < handle
     properties (Dependent)
         alpha    (1, :) double % Angle of attack, [rad]
         reynolds (1, :) double % Reynolds number, [-]
+        massflow (1, :) double % Mass flow rate through the element, [kg/s]
         indVelAx (1, :) double % Induced axial velocity at rotor disk, [m/s]
         indVelTg (1, :) double % Induced tangential velocity at rotor disk, [m/s]
         inflowRat (1, :) double % Inflow ratio, [-]
@@ -96,7 +97,7 @@ classdef ElemPerf < handle
     % Cache for dependent properties to avoid excessive recalculation
     properties (Access = private, Hidden)
         alpha_    (1, :) double % Angle of attack, [rad]
-        reynolds_ (1, :) double % Reynolds number, [-]
+        massflow_ (1, :) double % Mass flow rate through the element, [kg/s]
         indVelAx_ (1, :) double % Induced axial velocity at rotor disk, [m/s]
         indVelTg_ (1, :) double % Induced tangential velocity at rotor disk, [m/s]
         inflowRat_ (1, :) double % Inflow ratio, [-]
@@ -176,6 +177,10 @@ classdef ElemPerf < handle
 
         end
 
+        function massflow = get.massflow(self)
+            massflow = self.massflow_;
+        end
+
         function indVelAx = get.indVelAx(self)
             indVelAx = self.indVelAx_;
         end
@@ -211,6 +216,10 @@ classdef ElemPerf < handle
             [self.cl, self.cd] = getclcd(self, val);
         end
 
+        function set.massflow(self, val)
+            self.massflow_ = val;
+        end
+
         function set.indVelAx(self, val)
             % Cache value
             self.indVelAx_ = val;
@@ -271,6 +280,7 @@ classdef ElemPerf < handle
         calcforces(self) % Calculate forces, torque and power
         [cl, cd] = getclcd(self, aoaVect, reyVect, i) % Get values of cl and cd
         plotveltriangles(self, nTriangles, varargin)
+        updateupstreamvel(self, PrevOpRot)
     end
 
 end
diff --git a/src/classes/@ElemPerf/calcforces.m b/src/classes/@ElemPerf/calcforces.m
index f6700dc..6d41711 100644
--- a/src/classes/@ElemPerf/calcforces.m
+++ b/src/classes/@ElemPerf/calcforces.m
@@ -39,7 +39,7 @@ function calcforces(self)
     if ~isempty(self.cl)
 
         axVel = self.upstreamVelAx + self.indVelAx;
-        tgVel = self.tgSpeed - self.indVelTg;
+        tgVel = self.tgSpeed - self.Rot.Bl.spinDir * (self.upstreamVelTg + self.indVelTg);
         relVel = sqrt(axVel.^2 + tgVel.^2);
 
         % Non-dimensionalization factor
diff --git a/src/classes/@ElemPerf/updateupstreamvel.m b/src/classes/@ElemPerf/updateupstreamvel.m
new file mode 100644
index 0000000..cea2781
--- /dev/null
+++ b/src/classes/@ElemPerf/updateupstreamvel.m
@@ -0,0 +1,85 @@
+function updateupstreamvel(self, PrevOpRot)
+    % UPDATEUPSTREAMVEL Update axial and tangential upstream velocities.
+    % This function is used to update upstream velocities of the current
+    % rotor based on the previous rotor inforations.
+    %
+    % Syntax:
+    %    ElPerf.updateupstreamvel(PrevOpRot) updates upstream velocities in an ElPerf
+    %    object according to the performance of the previous Operating Rotor `PrevOpRot`.
+    %
+    % Inputs:
+    %   - prevOpRot : previous Operating Rotor object
+    %
+    % Outputs:
+    %    Updates the following properties of the ElemPerf object:
+    %       - self.upstreamVelAx  : Upstream axial velocity above rotor disk, [m/s]
+    %       - self.upstreamVelTg  : Upstream tangential velocity above rotor disk, [m/s]
+    %
+    % It is assumed that :
+    %       - a simplified multiple stream tube model is adopted
+    %       - there is no contraction between upstream and rotor levels for the current rotor
+    %       - the swirl is taken into account
+    %
+    % See also: ElemPerf.
+    %
+    % <a href="https:/gitlab.uliege.be/rotare/documentation">Complete documentation (online)</a>
+
+    % ----------------------------------------------------------------------------------------------
+    % (c) Copyright 2022-2023 University of Liege
+    % Author: Thomas Lambert <t.lambert@uliege.be>
+    % ULiege - Aeroelasticity and Experimental Aerodynamics
+    % MIT License
+    % Repo: https://gitlab.uliege.be/rotare/rotare
+    % Docs: https://gitlab.uliege.be/rotare/documentation
+    % Issues: https://gitlab.uliege.be/rotare/rotare/-/issues
+    %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+    % axial velocity at the previous rotor disk
+    prevRotVelAx = PrevOpRot.ElPerf.upstreamVelAx + PrevOpRot.ElPerf.indVelAx;
+
+    % axial downstream velocity for previous rotor
+    prevDownVelAx = PrevOpRot.ElPerf.upstreamVelAx + 2 * PrevOpRot.ElPerf.indVelAx;
+
+    % contraction variables
+    % radius ratio of of previous rotor contraction
+    vec_areaRatio = prevRotVelAx ./ prevDownVelAx;
+    % area ratio of previous rotor contraction
+    vec_radiusRatio = sqrt(vec_areaRatio);
+
+    vcRadius = PrevOpRot.Rot.radius * vec_radiusRatio(1, end); % vena contracta radius
+    vcRadIdx = find(self.Rot.Bl.y <= vcRadius, 1, 'last');
+
+    % tangential velocity at the previous rotor disk
+    prevDownVelTg = PrevOpRot.ElPerf.dQ ./ (PrevOpRot.Rot.Bl.y .* ...
+                                            PrevOpRot.ElPerf.massflow) .* (1 ./ vec_areaRatio);
+
+    % Mapping of velocities from initial upper rotor splitting to contracted splitting
+    % contracted previous rotor velocities at current rotor elements
+    yqueryAx = interp1(PrevOpRot.Rot.Bl.y .* vec_radiusRatio, prevDownVelAx, ...
+                       self.Rot.Bl.y, 'linear', 0);
+    yqueryTg = interp1(PrevOpRot.Rot.Bl.y .* vec_radiusRatio, prevDownVelTg, ...
+                       self.Rot.Bl.y, 'linear', 0);
+
+    % Update upstream axial velocity with previous rotor wake
+    self.upstreamVelAx(1, 1:vcRadIdx) = yqueryAx(1, 1:vcRadIdx);
+
+    % Update upstream tangential velocity with previous rotor wake
+    self.upstreamVelTg(1, 1:vcRadIdx) = yqueryTg(1, 1:vcRadIdx);
+
+    % Verification of mass flow conservation
+    % total flow mass at previous rotor level
+    debit_u2 = trapz(2 * pi * PrevOpRot.Op.Flow.rho * PrevOpRot.Rot.Bl.dy * ...
+                     PrevOpRot.Rot.Bl.y .* prevRotVelAx);
+    % total flow mass at downstream of previous rotor - There is contraction of wake
+    area_contracted = PrevOpRot.Rot.Bl.dy * PrevOpRot.Rot.Bl.y .* vec_areaRatio;
+    debit_u3 = trapz(2 * pi * PrevOpRot.Op.Flow.rho * area_contracted .* prevDownVelAx);
+    % total flow mass at upstream of current rotor
+    debit_d1 = trapz(2 * pi * self.Op.Flow.rho * ...
+                     self.Rot.Bl.dy * self.Rot.Bl.y(1, 1:vcRadIdx) .* ...
+                     self.upstreamVelAx(1, 1:vcRadIdx));
+    % comparison of the flow masses in percentage
+    % Remove the below semicolons to display the results before running the code :
+    diff = 100 * (debit_u2 - debit_u3) / debit_u2;
+    diff2 = 100 * (debit_u3 - debit_d1) / debit_u3;
+
+end
diff --git a/src/classes/@OperRotor/OperRotor.m b/src/classes/@OperRotor/OperRotor.m
index 4c5842c..bf62161 100644
--- a/src/classes/@OperRotor/OperRotor.m
+++ b/src/classes/@OperRotor/OperRotor.m
@@ -64,6 +64,8 @@ classdef OperRotor < handle
         cQ (1, 1) double  % Rotor torque coefficient, [-]
         cP (1, 1) double  % Rotor power coefficient, [-]
 
+        totalmassflow (1, 1) double  % Mass flow through the rotor, [kg/s]
+
         nonDim (1, 2) char % Non-dimensionalization factor ('US', 'EU')
     end
 
@@ -119,6 +121,10 @@ classdef OperRotor < handle
             cP = calccoeff(self, 'power');
         end
 
+        function totalmassflow = get.totalmassflow(self)
+            totalmassflow = trapz(self.ElPerf.massflow);
+        end
+
         function nonDim = get.nonDim(self)
             nonDim = self.nonDim_cached;
         end
diff --git a/src/classes/@Result/Result.m b/src/classes/@Result/Result.m
index 5230a0e..e37170a 100644
--- a/src/classes/@Result/Result.m
+++ b/src/classes/@Result/Result.m
@@ -43,10 +43,10 @@ classdef Result < handle
 
     % Base properties of the elements
     properties
-        leishman (1, :) OperRotor  % Results obtained with the leishman solver
-        indfact (1, :) OperRotor   % Results obtained with the indFact solver
-        indvel (1, :) OperRotor    % Results obtained with the indVel solver
-        stahlhut (1, :) OperRotor  % Results obtained with the stalhlut solver
+        leishman (:, :) OperRotor  % Results obtained with the leishman solver
+        indfact (:, :) OperRotor   % Results obtained with the indFact solver
+        indvel (:, :) OperRotor    % Results obtained with the indVel solver
+        stahlhut (:, :) OperRotor  % Results obtained with the stalhlut solver
         operPts table              % Table of operating points for each individual result
         ModExt (1, 1) struct       % Modelling enstension options (losses, etc)
     end
@@ -115,14 +115,17 @@ function resTable = maketable(self, prop)
 
     resTable.Properties.VariableNames = propName;
 
-    for iOp = 1:size(self.operPts, 1)
+    for iRotor = 1:size(self.indvel, 2)
         for iSolv = 1:length(self.SOLVERS)
-            solver = self.SOLVERS{iSolv};
+            for iOp = 1:size(self.operPts, 1)
 
-            if ~isempty(self.(solver))
-                resTable.(iSolv)(iOp) = self.(solver)(iOp).(prop);
-            else
-                resTable.(iSolv)(iOp) = NaN;
+                solver = self.SOLVERS{iSolv};
+
+                if ~isempty(self.(solver))
+                    resTable.(iSolv)(iOp, iRotor) = self.(solver)(iOp, iRotor).(prop);
+                else
+                    resTable.(iSolv)(iOp, iRotor) = NaN;
+                end
             end
         end
     end
diff --git a/src/classes/@Result/filter.m b/src/classes/@Result/filter.m
index 436858f..3b87b5c 100644
--- a/src/classes/@Result/filter.m
+++ b/src/classes/@Result/filter.m
@@ -6,7 +6,7 @@ function [filtered, idx] = filter(self, alt, speed, rpm, coll)
     % -----
     %
     % Syntax:
-    %   [filtered, idx] = Result.filter(alt, rpm, coll, speed) Return the Results of the four
+    %   [filtered, idx] = Result.filter(alt, speed, rpm, coll) Return the Results of the four
     %   solvers for the operating point defined by `alt`, `rpm`, `coll`, `speed`.
     %
     % Inputs:
@@ -61,7 +61,7 @@ function [filtered, idx] = filter(self, alt, speed, rpm, coll)
     for iSolv = 1:length(DEF.ALLOWED_SOLVERS)
         thissolv = DEF.ALLOWED_SOLVERS{iSolv};
         if ~isempty(self.(thissolv))
-            filtered.(thissolv) = self.(thissolv)(1, idx);
+            filtered.(thissolv) = self.(thissolv)(idx, :);
         else
             filtered.(thissolv) = self.(thissolv);
         end
diff --git a/src/configs/caradonna1981.m b/src/configs/caradonna1981.m
index e96cace..48410f2 100644
--- a/src/configs/caradonna1981.m
+++ b/src/configs/caradonna1981.m
@@ -115,6 +115,7 @@ Blade.nElem = 100;  % Number of blade elements, [-]
 
 % Rotor base position
 Blade.hubPos = [0, 0, 0]; % Rotor center position (used for coaxial rotors), [m]
+Blade.spinDir = 1;        % Rotor spin direction (used for coaxial rotors; 1 = cw, -1 = ccw)
 
 % ==================================================================================================
 % ======================================= Overwrites ===============================================
diff --git a/src/configs/knight1937.m b/src/configs/knight1937.m
index d28fce5..7e6ee22 100644
--- a/src/configs/knight1937.m
+++ b/src/configs/knight1937.m
@@ -110,3 +110,4 @@ Blade.nElem = 100;  % Number of blade elements, [-]
 
 % Rotor base position
 Blade.hubPos = [0, 0, 0]; % Rotor center position (used for coaxial rotors), [m]
+Blade.spinDir = 1;        % Rotor spin direction (used for coaxial rotors; 1 = cw, -1 = ccw)
diff --git a/src/configs/template.m b/src/configs/template.m
index dbd803e..4f6de7f 100644
--- a/src/configs/template.m
+++ b/src/configs/template.m
@@ -162,3 +162,4 @@ Blade.nElem = 100;  % Number of blade elements, [-]
 
 % Rotor base position
 Blade.hubPos = [0, 0, 0]; % Rotor center position (used for coaxial rotors), [m]
+Blade.spinDir = 1;        % Rotor spin direction (used for coaxial rotors; 1 = cw, -1 = ccw)
diff --git a/src/configs/templatecoax.m b/src/configs/templatecoax.m
index ccfa168..764d984 100644
--- a/src/configs/templatecoax.m
+++ b/src/configs/templatecoax.m
@@ -34,6 +34,7 @@ else
     rotorShift = [-1, 0, 0];
 end
 Blade(2).hubPos = Blade(1).hubPos + rotorShift; % Shift second rotor w.r.t the application type
+Blade(2).spinDir = -Blade(1).spinDir; % Spin in opposite direction from first rotor
 
 % ==================================================================================================
 % ===================================== Operating points ===========================================
diff --git a/src/rotare.m b/src/rotare.m
index 0a80671..efdf786 100644
--- a/src/rotare.m
+++ b/src/rotare.m
@@ -95,7 +95,7 @@ function [Results] = rotare(configFile)
 
     if Sim.Out.show3D
         % TODO: Allow plot for multiple rotors
-        Rot.plot('all', Sim.Out.hubType);
+        % Rot.plot('all', Sim.Out.hubType);
     end
 
     % (Following comment is so miss-hit metric checker stays happy)
diff --git a/src/solvers/bemt.m b/src/solvers/bemt.m
index 1f08f66..abb3deb 100644
--- a/src/solvers/bemt.m
+++ b/src/solvers/bemt.m
@@ -36,13 +36,13 @@ function bemt(OpRot, Mod)
     % Issues: https://gitlab.uliege.be/rotare/rotare/-/issues
     %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 
-    % For each rotor, calculate the external velocity and solve the BEMT equations
+    % Always calculate first rotor as if isolated
+    bemtsinglerot(OpRot(1), Mod);
 
-    for i = 1:length(OpRot)
-
-        % TODO: Coaxial: Determine proper external velocity to pass to the rotor here
+    for i = 2:length(OpRot)
 
         % Solves BEMT equations for the rotor using the true distribution of external velocity
+        OpRot(i).ElPerf.updateupstreamvel(OpRot(i - 1));
         bemtsinglerot(OpRot(i), Mod);
 
     end
diff --git a/src/solvers/indvel.m b/src/solvers/indvel.m
index 580da9a..e2b7592 100644
--- a/src/solvers/indvel.m
+++ b/src/solvers/indvel.m
@@ -119,6 +119,8 @@ function indvel(OpRot, Mod)
     OpRot.ElPerf.alpha = alpha;
     OpRot.ElPerf.indVelAx = v - OpRot.ElPerf.upstreamVelAx;
     OpRot.ElPerf.indVelTg = uw / 2;
+    OpRot.ElPerf.massflow = dmdot;
+
 end
 
 function force = coeff2force(coeff, OpRot, speed)
-- 
GitLab