def setUp(self): config = { 'drag coefficient': 0.7, 'inertia coefficient': 2.0, 'members': [{ 'end1': [0, 0, 0], 'end2': [0, 0, -10], 'diameter': 2.3, 'strip width': 1, }], } self.model = ViscousDragModel(config)
def Morison_added_mass(w, draft, radius, Cm=2.0): """Calculate added mass and damping from Morison strip model of uniform cylinder""" from whales.viscous_drag import ViscousDragModel Morison_model = ViscousDragModel({ 'inertia coefficient': Cm, 'members': [{ 'end1': [0, 0, 0], 'end2': [0, 0, -draft], 'diameter': 2 * radius, 'strip width': 1.0, }] }) A1 = Morison_model.Morison_added_mass() A = np.tile(A1, (len(w), 1, 1)) return LinearSystem(A, zeros_like(A), zeros_like(A))
def excitation_force(w, z1, z2, diameter, Cm=2): """Calculate the excitation force. Specific for now to vertical cylinders of varying diameter. ``z1``, ``z2``: coordinates of ends of element ``diameter``: array (2 x Npoints) diameter at each z value """ config = { 'end1': [0, 0, z1], 'end2': [0, 0, z2], 'diameter': diameter, 'strip width': 1.0, } Morison_model = ViscousDragModel({ 'inertia coefficient': Cm, 'members': [config], }) X = Morison_model.Morison_inertial_force(w) return X
def setUp(self): config = { 'drag coefficient': 0.7, 'members': [{ 'end1': [0, 0, 0], 'end2': [0, 0, -10], 'diameter': [[0, 3, 5, 10], [2.2, 2.2, 5, 5]], 'strip width': 0.5, }], } self.model = ViscousDragModel(config)
def added_mass(w, z1, z2, diameter, Cm=2): """Calculate the added mass/damping. Specific for now to vertical cylinders of varying diameter. ``z1``, ``z2``: coordinates of ends of element ``diameter``: array (2 x Npoints) diameter at each z value """ config = { 'end1': [0, 0, z1], 'end2': [0, 0, z2], 'diameter': diameter, 'strip width': 1.0, } Morison_model = ViscousDragModel({ 'inertia coefficient': Cm, 'members': [config], }) A1 = Morison_model.Morison_added_mass() A = np.tile(A1, (len(w), 1, 1)) return LinearSystem(A, zeros_like(A), zeros_like(A))
def __init__(self, config, frequency_values): self.config = config self.w = frequency_values # Build structural model self.structure = FloatingTurbineStructure(config['structure']) # Get hydrodynamic/static information h = config['hydrodynamics'] rho = config['constants']['water density'] first_order = WAMITData(h['WAMIT path'], water_density=rho) if 'QTF path' in h: second_order = SecondOrderData(h['QTF path'], water_density=rho) else: second_order = None self.hydro_info = HydrodynamicsInfo(first_order, second_order) # Get mooring line behaviour l = config['mooring']['linear'] self.mooring_static = np.asarray(l['static force']) self.mooring_stiffness = np.asarray(l['stiffness']) # Extra damping self.B_extra = np.asarray(h.get('extra damping', np.zeros(6))) if self.B_extra.ndim == 1: self.B_extra = np.diag(self.B_extra) # Viscous drag model if 'Morison elements' in h: self.morison = ViscousDragModel(h['Morison elements']) else: self.morison = None self.Bv = np.zeros((6, 6)) self.Fvc = np.zeros(6) self.Fvv = np.zeros((len(self.w), 6)) # Wave-drift damping self.Bwd = np.zeros((6, 6))
def setUp(self): config = { 'drag coefficient': 0.7, 'members': [{ # # Member in x-direction # 'end1': [0, 0, 0], # 'end2': [1, 0, 0], # 'diameter': 1, # 'strip width': 1, # }, { # Member in z-direction 'end1': [0, 0, 0], 'end2': [0, 0, 1], 'diameter': 1, 'strip width': 1, # }, { # # Member in xy plane at 30deg from x axis # 'end1': [0, 0, 0], # 'end2': [np.cos(30*np.pi/180), np.sin(30*np.pi/180), 0], # 'diameter': 1, # 'strip width': 1, }], } self.model = ViscousDragModel(config)
class FloatingTurbineModel(object): def __init__(self, config, frequency_values): self.config = config self.w = frequency_values # Build structural model self.structure = FloatingTurbineStructure(config['structure']) # Get hydrodynamic/static information h = config['hydrodynamics'] rho = config['constants']['water density'] first_order = WAMITData(h['WAMIT path'], water_density=rho) if 'QTF path' in h: second_order = SecondOrderData(h['QTF path'], water_density=rho) else: second_order = None self.hydro_info = HydrodynamicsInfo(first_order, second_order) # Get mooring line behaviour l = config['mooring']['linear'] self.mooring_static = np.asarray(l['static force']) self.mooring_stiffness = np.asarray(l['stiffness']) # Extra damping self.B_extra = np.asarray(h.get('extra damping', np.zeros(6))) if self.B_extra.ndim == 1: self.B_extra = np.diag(self.B_extra) # Viscous drag model if 'Morison elements' in h: self.morison = ViscousDragModel(h['Morison elements']) else: self.morison = None self.Bv = np.zeros((6, 6)) self.Fvc = np.zeros(6) self.Fvv = np.zeros((len(self.w), 6)) # Wave-drift damping self.Bwd = np.zeros((6, 6)) def calculate_viscous_effects(self, S_wave): """ Calculate viscous forces and damping with the given wave spectrum. Note that this can be iterative, as the transfer functions will be re-calculated using previously-calculated viscous effects. """ assert len(S_wave) == len(self.w) H_wave = self.transfer_function_from_wave_elevation() self.Bv, self.Fvc, self.Fvv = self.morison.total_drag(self.w, H_wave, S_wave) # Fvv is the actual force/unit wave height at each wave # frequency -- to get the spectrum, it's like the first-order # wave force spectrum. self.viscous_force_spectrum = response_spectrum(self.Fvv, S_wave) def calculate_wave_drift_damping(self, S_wave): """ Calculate and save wave-drift damping matrix """ assert len(S_wave) == len(self.w) self.Bwd = self.hydro_info.wave_drift_damping(self.w, S_wave) def linearised_matrices(self, w, **kwargs): """ Linearise the structural model and add in the hydrodynamic added-mass and damping for the given frequency ``w``, as well as the hydrostatic stiffness, wave-drift damping and viscous damping. """ # Set default perturbation kwargs.setdefault('perturbation', 1e-4) # Structural - includes gravitational stiffness M, B, C, = self.structure.linearised_matrices(**kwargs) M[:6, :6] += self.hydro_info.A(w) B[:6, :6] += self.hydro_info.B(w) + self.Bv + self.Bwd + self.B_extra C[:6, :6] += self.hydro_info.C + self.mooring_stiffness return M, B, C def coupled_modes(self, w, ignore_damping=False, **kwargs): M, B, C = self.linearised_matrices(w, **kwargs) if ignore_damping: wn, vn = linalg.eig(C, M) order = np.argsort(abs(wn)) wn = np.sqrt(abs(wn[order])) vn = vn[:, order] else: AA = r_[c_[zeros_like(C), C], c_[C, B]] BB = r_[c_[C, zeros_like(C)], c_[zeros_like(C), -M]] wn, vn = linalg.eig(AA, BB) order = np.argsort(abs(wn)) wn = abs(wn[order]) # Mode shapes are the first half of the rows; the second # half of the rows should be same multiplied by eigenvalues. vn = vn[:M.shape[0], order] # We expect all the modes to be complex conjugate; return # every other one. # First: make sure all are the same sign norm_vn = vn / vn[np.argmax(abs(vn), axis=0), range(vn.shape[1])] assert (np.allclose(wn[::2], wn[1::2], rtol=1e-4) and np.allclose(norm_vn[:, ::2], norm_vn[:, 1::2].conj(), atol=1e-2)), \ "Expect conjugate modes" wn = wn[::2] vn = norm_vn[:, ::2] return wn, vn def transfer_function(self, rotor_speed=None): """ Linearise the structural model and return multi-dof transfer function for the structure, including mooring lines and hydrodynamics, at the given frequencies ``ws`` """ # Structural - includes gravitational stiffness if rotor_speed is not None: M_struct, B_struct, C_struct = self.structure.linearised_matrices( zd0={'shaft': [rotor_speed]}, mbc=True) else: M_struct, B_struct, C_struct = self.structure.linearised_matrices() # Full matrices to be assembled at each frequency Mi = zeros_like(M_struct) Bi = zeros_like(B_struct) Ci = zeros_like(C_struct) # Stiffness is constant -- add hydrostatics and mooring lines hh = self.hydro_info # shorthand Ci[:, :] = C_struct Ci[:6, :6] += hh.C + self.mooring_stiffness # Calculate transfer function at each frequency H = np.empty((len(self.w),) + M_struct.shape, dtype=np.complex) for i, w in enumerate(self.w): Mi[:, :] = M_struct Bi[:, :] = B_struct Mi[:6, :6] += hh.A(w) # add rigid-body parts Bi[:6, :6] += hh.B(w) + self.Bv + self.Bwd + self.B_extra H[i, :, :] = linalg.inv(-(w**2)*Mi + 1j*w*Bi + Ci) return H def transfer_function_from_wave_elevation(self, rotor_speed=None, heading=0): """ Calculate the transfer function from wave elevation to response, similar to ``transfer_function(ws)`` but including the wave excitation force ``X``. """ H = self.transfer_function(rotor_speed) X = self.hydro_info.X(self.w, heading) # interpolate # Add in the viscous drag force due to waves X += self.Fvv X[self.w == 0] += self.Fvc # constant drag force # Multiply transfer functions to get overall transfer function # (H can be larger than X if the model is flexible) H_wave = np.einsum('wij,wj->wi', H[:, :, :6], X) return H_wave def response_spectrum(self, S_wave, second_order=True, viscous=True): """Convenience method which calculates the response spectrum """ S1 = self.hydro_info.first_order_force_spectrum(self.w, S_wave) if second_order: S2 = self.hydro_info.second_order_force_spectrum(self.w, S_wave) else: S2 = zeros_like(S1) if viscous: self.calculate_viscous_effects(S_wave) Sv = self.viscous_force_spectrum else: Sv = zeros_like(S1) self.calculate_wave_drift_damping(S_wave) # XXX this doesn't work well if second_order or viscous is set # to False -- transfer_function() will use previously-calculated values H = self.transfer_function() SF = S1 + S2 + Sv Sx = response_spectrum(H[:, :, :6], SF) return Sx @classmethod def from_yaml(cls, filename, freq): # Read the data with open(filename) as f: config = yaml.safe_load(f) # Load the config file from the same directory with path(filename).abspath().parent: return cls(config, freq)
class CylinderTestCase(MyTestCase): """Test simple model with vertical cylinder""" def setUp(self): config = { 'drag coefficient': 0.7, 'inertia coefficient': 2.0, 'members': [{ 'end1': [0, 0, 0], 'end2': [0, 0, -10], 'diameter': 2.3, 'strip width': 1, }], } self.model = ViscousDragModel(config) def test_model_elements(self): """Check element strips setup correctly""" m = self.model self.assertArraysEqual(m.element_lengths, np.ones(10)) # Centres of strips centres = np.zeros((10, 3)) centres[:,2] = -np.arange(0.5, 10, 1) self.assertArraysEqual(m.element_centres, centres) # Element diameters self.assertArraysEqual(m.element_diameters, 2.3) # Element axes self.assertArraysEqual(m.element_axes, np.array([np.eye(3)] * 10)) def test_wave_velocity_transfer_func(self): """Test wave velocity transfer function""" w = np.array([1,2]) # frequencies to test H_uf = self.model.wave_velocity_transfer_function(w) # With waves in x-direction, sideways velocity should be zero self.assertArraysEqual(H_uf[:,:,1], 0) # Check variation in depth: exp(kz) iz1 = 3 iz2 = 8 z = self.model.element_centres[:,2] for i in range(2): assert_array_almost_equal_nulp(H_uf[i,iz1,:] / np.exp(w[i]**2/9.81*z[iz1]), H_uf[i,iz2,:] / np.exp(w[i]**2/9.81*z[iz2])) # Check all x velocities are in-phase and real, all z are imaginary self.assertTrue(np.isreal( H_uf[:,:,0]).all()) self.assertTrue(np.isreal(1j * H_uf[:,:,2]).all()) def test_structural_velocity_transfer_func(self): """Test structural velocity with special cases""" w = np.array([1,2]) # frequencies to test # Case 1: pure surge motion H1 = np.zeros((2, 6)) # shape (freq, xyzXYZ) H1[:,0] = 1 # maximum surge at maximum datum wave height H_us = self.model.structural_velocity_transfer_function(w, H1) # all elements should have same surge velocity; all other velocities zero # at t=0, velocity is zero and becoming negative 90 deg later self.assertArraysEqual(H_us[1,:,0], 2j) self.assertArraysEqual(H_us[0,:,0], 1j) self.assertArraysEqual(H_us[:,:,1:], 0) # Case 2: pure roll motion H2 = np.zeros((2, 6)) # shape (freq, xyzXYZ) H2[:,3] = 1 # maximum roll at maximum datum wave height H_us = self.model.structural_velocity_transfer_function(w, H2) # x & z velocity should be zero self.assertArraysEqual(H_us[:,:,[0,2]], 0) # y velocity corresponding to rotation about origin (check bottom) # at t=0, ang. velocity is zero and becoming negative 90 deg later # Velocity of bottom element = 9.5 * ang vel self.assertArraysEqual(H_us[0,-1,1], 9.5 * 1j) self.assertArraysEqual(H_us[1,-1,1], 9.5 * 2j) def test_added_mass(self): """Test added mass calculation from Morison elements""" A = self.model.Morison_added_mass() # Expected surge added mass: (Cm-1) * rho * V self.assertEqual(A[0,0], 1 * 1025 * 10 * np.pi * 2.3**2 / 4)