alm_element.f90 Source File


This file depends on

sourcefile~~alm_element.f90~~EfferentGraph sourcefile~alm_element.f90 alm_element.f90 sourcefile~alm_airfoil.f90 alm_airfoil.f90 sourcefile~alm_element.f90->sourcefile~alm_airfoil.f90 sourcefile~common.f90~4 common.f90 sourcefile~alm_element.f90->sourcefile~common.f90~4 sourcefile~logger.f90 logger.f90 sourcefile~alm_element.f90->sourcefile~logger.f90 sourcefile~alm_airfoil.f90->sourcefile~common.f90~4 sourcefile~alm_airfoil.f90->sourcefile~logger.f90 sourcefile~common.f90~4->sourcefile~logger.f90

Files dependent on this one

sourcefile~~alm_element.f90~~AfferentGraph sourcefile~alm_element.f90 alm_element.f90 sourcefile~alm.f90 alm.f90 sourcefile~alm.f90->sourcefile~alm_element.f90 sourcefile~alm_output.f90 alm_output.f90 sourcefile~alm.f90->sourcefile~alm_output.f90 sourcefile~alm_turbine.f90 alm_turbine.f90 sourcefile~alm.f90->sourcefile~alm_turbine.f90 sourcefile~alm_output.f90->sourcefile~alm_element.f90 sourcefile~alm_turbine.f90->sourcefile~alm_element.f90 sourcefile~turbine_setup.f90 turbine_setup.f90 sourcefile~turbine_setup.f90->sourcefile~alm.f90 sourcefile~abl.f90~2 abl.f90 sourcefile~abl.f90~2->sourcefile~turbine_setup.f90 sourcefile~wind_turbine.f90 wind_turbine.f90 sourcefile~wind_turbine.f90->sourcefile~turbine_setup.f90 sourcefile~build.f90 build.f90 sourcefile~build.f90->sourcefile~abl.f90~2 sourcefile~case_factory.f90 case_factory.f90 sourcefile~build.f90->sourcefile~case_factory.f90 sourcefile~case_factory.f90->sourcefile~abl.f90~2 sourcefile~case_factory.f90->sourcefile~wind_turbine.f90 sourcefile~memcheck.f90 memcheck.f90 sourcefile~memcheck.f90->sourcefile~build.f90 sourcefile~xcompact.f90 xcompact.f90 sourcefile~xcompact.f90->sourcefile~case_factory.f90

Source Code

module m_alm_element
  !! Actuator lines for the actuator line model (ALM): blade-element geometry,
  !! rigid-body kinematics and aerodynamic loads.
  !!
  !! Ported from Incompact3d's `acl_elem.f90` and `acl_utils.f90`. An actuator
  !! line is defined by its stations (element ends) on the quarter-chord line;
  !! element centres, unit vectors (spanwise s, chordwise t, normal n), areas
  !! and chords are derived from them by make_line_geometry. Loads come from
  !! the static airfoil polars,
  !! \[
  !! \alpha = \operatorname{atan2}(u_n, u_t),\qquad
  !! \mathbf{F} = \tfrac12\rho\,U_r^2 A\,F_{end}
  !!   \left(C_N\mathbf{n} + C_T\mathbf{t}\right),
  !! \]
  !! with \(C_N = C_L\cos\alpha + C_D\sin\alpha\),
  !! \(C_T = -C_L\sin\alpha + C_D\cos\alpha\) and \(u_n, u_t\) the normal and
  !! chordwise components of the fluid velocity relative to the element.
  !!
  !! The dynamic-stall, added-mass, random-walk and constant-circulation
  !! options of Incompact3d are not ported.
  use m_common, only: dp, pi
  use m_alm_airfoil, only: airfoil_t, eval_static_coeffs

  implicit none

  private
  public :: actuator_line_t, allocate_line, make_line_geometry, &
            rotate_line, pitch_line, compute_line_forces, &
            compute_tower_forces, populate_line_airfoils, &
            read_line_geometry, quat_rot, cross

  real(dp), parameter, public :: deg2rad = pi/180._dp

  type :: actuator_line_t
    character(len=256) :: name = ''
    integer :: n_elem = 0
    logical :: flip_n = .false.        !! flip the normal (clockwise rotor)
    real(dp) :: cor(3) = 0._dp         !! centre of rotation
    real(dp) :: length = 0._dp         !! reference length (blade radius)

    ! Station (element end) values
    real(dp), allocatable :: qc(:, :)   !! (3, n_elem + 1) quarter-chord points
    real(dp), allocatable :: t(:, :)    !! (3, n_elem + 1) chordwise unit vec
    real(dp), allocatable :: chord(:)
    real(dp), allocatable :: thick(:)   !! thickness to chord ratio
    real(dp), allocatable :: pitch(:)   !! station pitch [rad]

    ! Element values
    real(dp), allocatable :: pe(:, :)   !! (3, n_elem) element centres
    real(dp), allocatable :: te(:, :)   !! chordwise unit vector
    real(dp), allocatable :: ne(:, :)   !! normal unit vector
    real(dp), allocatable :: se(:, :)   !! spanwise unit vector
    real(dp), allocatable :: ec(:)      !! chord
    real(dp), allocatable :: eds(:)     !! spanwise length
    real(dp), allocatable :: earea(:)   !! planform area
    real(dp), allocatable :: erdist(:)  !! distance from cor
    real(dp), allocatable :: ettoc(:)   !! thickness to chord ratio
    real(dp), allocatable :: epitch(:)  !! pitch [rad]
    integer, allocatable :: eairfoil(:) !! index into the owner's airfoils

    ! Flow and body velocities at the elements
    real(dp), allocatable :: evel(:, :) !! (3, n_elem) sampled fluid velocity
    real(dp), allocatable :: evb(:, :)  !! (3, n_elem) body velocity

    ! Element aerodynamics
    real(dp), allocatable :: eaoa(:)    !! angle of attack [rad]
    real(dp), allocatable :: ere(:)     !! chord Reynolds number
    real(dp), allocatable :: eur(:)     !! relative speed
    real(dp), allocatable :: ecl(:), ecd(:), ecm(:), ecn(:), ect(:)
    real(dp), allocatable :: efn(:), eft(:), ems(:)
    real(dp), allocatable :: ef(:, :)   !! (3, n_elem) force [N]
    real(dp), allocatable :: end_factor(:) !! tip/root loss factor
  end type actuator_line_t

contains

  subroutine allocate_line(line, n_stations)
    !! (Re)allocate the station and element arrays of `line`.
    implicit none
    type(actuator_line_t), intent(inout) :: line
    integer, intent(in) :: n_stations

    integer :: ne

    ne = n_stations - 1
    line%n_elem = ne
    call realloc2(line%qc, n_stations)
    call realloc2(line%t, n_stations)
    call realloc1(line%chord, n_stations)
    call realloc1(line%thick, n_stations)
    call realloc1(line%pitch, n_stations)
    call realloc2(line%pe, ne)
    call realloc2(line%te, ne)
    call realloc2(line%ne, ne)
    call realloc2(line%se, ne)
    call realloc1(line%ec, ne)
    call realloc1(line%eds, ne)
    call realloc1(line%earea, ne)
    call realloc1(line%erdist, ne)
    call realloc1(line%ettoc, ne)
    call realloc1(line%epitch, ne)
    call realloc2(line%evel, ne)
    call realloc2(line%evb, ne)
    call realloc1(line%eaoa, ne)
    call realloc1(line%ere, ne)
    call realloc1(line%eur, ne)
    call realloc1(line%ecl, ne)
    call realloc1(line%ecd, ne)
    call realloc1(line%ecm, ne)
    call realloc1(line%ecn, ne)
    call realloc1(line%ect, ne)
    call realloc1(line%efn, ne)
    call realloc1(line%eft, ne)
    call realloc1(line%ems, ne)
    call realloc2(line%ef, ne)
    call realloc1(line%end_factor, ne)
    if (allocated(line%eairfoil)) deallocate (line%eairfoil)
    allocate (line%eairfoil(ne))
    line%eairfoil = 1
    line%end_factor = 1._dp

  contains

    subroutine realloc1(a, n)
      real(dp), allocatable, intent(inout) :: a(:)
      integer, intent(in) :: n
      if (allocated(a)) deallocate (a)
      allocate (a(n))
      a = 0._dp
    end subroutine realloc1

    subroutine realloc2(a, n)
      real(dp), allocatable, intent(inout) :: a(:, :)
      integer, intent(in) :: n
      if (allocated(a)) deallocate (a)
      allocate (a(3, n))
      a = 0._dp
    end subroutine realloc2
  end subroutine allocate_line

  subroutine read_line_geometry(filename, length, span_vec, r_over_l, &
                                c_over_l, pitch, thick)
    !! Read an actuator line geometry (`.al`) file:
    !!   `<label>: <length>`, `<label>: <sx> <sy> <sz>`, `<label>: <n>`,
    !! then n rows of `r/L  c/L  pitch[deg]  t/c`.
    use m_logger, only: abort_run
    implicit none
    character(len=*), intent(in) :: filename
    real(dp), intent(out) :: length, span_vec(3)
    real(dp), allocatable, intent(out) :: r_over_l(:), c_over_l(:), &
                                          pitch(:), thick(:)

    character(len=1024) :: line
    integer :: unit, ios, n, i

    open (newunit=unit, file=trim(filename), status='old', action='read', &
          iostat=ios)
    if (ios /= 0) call abort_run('ALM: cannot open geometry file: ' &
                                 //trim(filename))
    read (unit, '(A)', iostat=ios) line
    if (ios == 0) read (line(index(line, ':') + 1:), *, iostat=ios) length
    if (ios == 0) read (unit, '(A)', iostat=ios) line
    if (ios == 0) read (line(index(line, ':') + 1:), *, iostat=ios) span_vec
    if (ios == 0) read (unit, '(A)', iostat=ios) line
    if (ios == 0) read (line(index(line, ':') + 1:), *, iostat=ios) n
    if (ios /= 0) call abort_run('ALM: bad header in geometry file: ' &
                                 //trim(filename))
    if (n < 2) call abort_run('ALM: geometry file needs at least two &
                              &stations: '//trim(filename))

    allocate (r_over_l(n), c_over_l(n), pitch(n), thick(n))
    do i = 1, n
      read (unit, *, iostat=ios) r_over_l(i), c_over_l(i), pitch(i), thick(i)
      if (ios /= 0) call abort_run('ALM: bad station row in geometry file: ' &
                                   //trim(filename))
    end do
    close (unit)
  end subroutine read_line_geometry

  subroutine make_line_geometry(line)
    !! Element centres, unit vectors, areas and chords from the stations.
    implicit none
    type(actuator_line_t), intent(inout) :: line

    real(dp) :: s_e(3), t_e(3), n_e(3), p1(3), p2(3), p3(3), p4(3)
    real(dp) :: a1(3), a2(3), s_mag
    integer :: j

    do j = 1, line%n_elem
      line%pe(:, j) = (line%qc(:, j + 1) + line%qc(:, j))/2._dp
      line%erdist(j) = norm2(line%pe(:, j) - line%cor)

      ! Spanwise along the quarter-chord line
      s_e = line%qc(:, j + 1) - line%qc(:, j)
      s_mag = sqrt(dot_product(s_e, s_e))
      line%eds(j) = s_mag
      s_e = s_e/s_mag

      ! Mean chordwise vector, made normal to the spanwise one
      t_e = (line%t(:, j + 1) + line%t(:, j))/2._dp
      t_e = t_e - dot_product(t_e, s_e)*s_e
      t_e = t_e/sqrt(dot_product(t_e, t_e))

      n_e = cross(s_e, t_e)
      n_e = n_e/sqrt(dot_product(n_e, n_e))

      line%se(:, j) = s_e
      line%te(:, j) = t_e
      line%ne(:, j) = n_e
      if (line%flip_n) then
        line%ne(:, j) = -n_e
        line%se(:, j) = -s_e
      end if

      ! Element planform: quad from the leading and trailing edges of both
      ! stations, split into two triangles
      p1 = line%qc(:, j) - 0.25_dp*line%chord(j)*line%t(:, j)
      p2 = line%qc(:, j) + 0.75_dp*line%chord(j)*line%t(:, j)
      p3 = line%qc(:, j + 1) + 0.75_dp*line%chord(j + 1)*line%t(:, j + 1)
      p4 = line%qc(:, j + 1) - 0.25_dp*line%chord(j + 1)*line%t(:, j + 1)
      a1 = cross(p2 - p1, p3 - p2)/2._dp
      a2 = cross(p4 - p3, p1 - p4)/2._dp
      line%earea(j) = sqrt(dot_product(a1, a1)) + sqrt(dot_product(a2, a2))

      line%ec(j) = line%earea(j)/s_mag
      line%ettoc(j) = 0.5_dp*(line%thick(j + 1) + line%thick(j))
      line%epitch(j) = 0.5_dp*(line%pitch(j + 1) + line%pitch(j))
    end do
  end subroutine make_line_geometry

  subroutine rotate_line(line, origin, axis, theta)
    !! Rotate the stations of `line` by `theta` about `axis` through `origin`
    !! (chordwise vectors about `axis` through zero). Element geometry is not
    !! rebuilt; call make_line_geometry afterwards.
    implicit none
    type(actuator_line_t), intent(inout) :: line
    real(dp), intent(in) :: origin(3), axis(3), theta

    integer :: i

    do i = 1, line%n_elem + 1
      line%qc(:, i) = quat_rot(line%qc(:, i), theta, axis, origin)
      line%t(:, i) = quat_rot(line%t(:, i), theta, axis, [0._dp, 0._dp, 0._dp])
      line%t(:, i) = line%t(:, i)/norm2(line%t(:, i))
    end do
  end subroutine rotate_line

  subroutine pitch_line(line, pitch_angle)
    !! Pitch every station by `pitch_angle` [rad] about the line's root-to-tip
    !! direction, then rebuild the element geometry.
    implicit none
    type(actuator_line_t), intent(inout) :: line
    real(dp), intent(in) :: pitch_angle

    real(dp) :: s(3)
    integer :: i, n_st

    n_st = line%n_elem + 1
    s = line%qc(:, n_st) - line%cor
    s = s/norm2(s)
    do i = 1, n_st
      line%t(:, i) = quat_rot(line%t(:, i), pitch_angle, s, &
                              [0._dp, 0._dp, 0._dp])
      line%t(:, i) = line%t(:, i)/norm2(line%t(:, i))
      line%pitch(i) = line%pitch(i) - pitch_angle
    end do
    call make_line_geometry(line)
  end subroutine pitch_line

  subroutine populate_line_airfoils(line, airfoils)
    !! Give every element the airfoil whose thickness ratio is closest to its
    !! own, clamped to the thinnest and thickest available.
    implicit none
    type(actuator_line_t), intent(inout) :: line
    type(airfoil_t), intent(in) :: airfoils(:)

    real(dp) :: thicks(size(airfoils))
    integer :: ie, imin, imax, a

    do a = 1, size(airfoils)
      thicks(a) = airfoils(a)%tc
    end do
    imin = minloc(thicks, 1)
    imax = maxloc(thicks, 1)
    do ie = 1, line%n_elem
      if (line%ettoc(ie) >= thicks(imax)) then
        line%eairfoil(ie) = imax
      else if (line%ettoc(ie) <= thicks(imin)) then
        line%eairfoil(ie) = imin
      else
        line%eairfoil(ie) = minloc(abs(thicks - line%ettoc(ie)), 1)
      end if
    end do
  end subroutine populate_line_airfoils

  subroutine compute_line_forces(line, airfoils, rho_air, visc)
    !! Blade-element loads of every element from the sampled fluid velocity,
    !! the body velocity and the static polars.
    implicit none
    type(actuator_line_t), intent(inout) :: line
    type(airfoil_t), intent(in) :: airfoils(:)
    real(dp), intent(in) :: rho_air, visc

    real(dp) :: urel(3), urdn, urdc, ur, alpha, cl, cd, cm25, cn, ct
    real(dp) :: fn, ft, ms
    integer :: ie

    do ie = 1, line%n_elem
      urel = line%evel(:, ie) - line%evb(:, ie)
      urdn = dot_product(line%ne(:, ie), urel)
      urdc = dot_product(line%te(:, ie), urel)
      ur = sqrt(urdn**2 + urdc**2)
      alpha = atan2(urdn, urdc)

      line%eur(ie) = ur
      line%eaoa(ie) = alpha
      line%ere(ie) = ur*line%ec(ie)/visc

      call eval_static_coeffs(airfoils(line%eairfoil(ie)), line%ere(ie), &
                              alpha/deg2rad, cl, cd, cm25)

      cn = cl*cos(alpha) + cd*sin(alpha)
      ct = -cl*sin(alpha) + cd*cos(alpha)

      fn = 0.5_dp*rho_air*cn*line%earea(ie)*ur**2
      ft = 0.5_dp*rho_air*ct*line%earea(ie)*ur**2
      ms = 0.5_dp*rho_air*cm25*line%ec(ie)*line%earea(ie)*ur**2

      fn = fn*line%end_factor(ie)
      ft = ft*line%end_factor(ie)

      line%ecl(ie) = cl
      line%ecd(ie) = cd
      line%ecm(ie) = cm25
      line%ecn(ie) = cn
      line%ect(ie) = ct
      line%efn(ie) = fn
      line%eft(ie) = ft
      line%ems(ie) = ms
      line%ef(:, ie) = fn*line%ne(:, ie) + ft*line%te(:, ie)
    end do
  end subroutine compute_line_forces

  subroutine compute_tower_forces(tower, rho_air, visc, time, cl, cd, &
                                  strouhal, noise)
    !! Tower loads: constant drag and a lift oscillating at the shedding
    !! frequency St*U/D, perturbed by `noise` (uniform in [0, 1) per element).
    implicit none
    type(actuator_line_t), intent(inout) :: tower
    real(dp), intent(in) :: rho_air, visc, time, cl, cd, strouhal
    real(dp), intent(in) :: noise(:)

    real(dp) :: urdn, urdc, ur, alpha, freq, cn, ct, fn, ft
    integer :: ie

    do ie = 1, tower%n_elem
      urdn = dot_product(tower%ne(:, ie), tower%evel(:, ie))
      urdc = dot_product(tower%te(:, ie), tower%evel(:, ie))
      ur = sqrt(urdn**2 + urdc**2)
      alpha = atan2(urdn, urdc)
      tower%eur(ie) = ur
      tower%eaoa(ie) = alpha
      tower%ere(ie) = ur*tower%ec(ie)/visc

      freq = strouhal*ur/max(tower%ec(ie), 0.0001_dp)
      tower%ecl(ie) = cl*sin(2._dp*freq*pi*time) &
                      *(1._dp + 0.05_dp*(-1._dp + 2._dp*noise(ie)))
      tower%ecd(ie) = cd

      cn = tower%ecl(ie)*cos(alpha) + tower%ecd(ie)*sin(alpha)
      ct = -tower%ecl(ie)*sin(alpha) + tower%ecd(ie)*cos(alpha)
      fn = 0.5_dp*rho_air*cn*tower%earea(ie)*ur**2
      ft = 0.5_dp*rho_air*ct*tower%earea(ie)*ur**2

      tower%ecn(ie) = cn
      tower%ect(ie) = ct
      tower%efn(ie) = fn
      tower%eft(ie) = ft
      tower%ems(ie) = 0._dp
      tower%ef(:, ie) = fn*tower%ne(:, ie) + ft*tower%te(:, ie)
    end do
  end subroutine compute_tower_forces

  pure function quat_rot(v, theta, axis, origin) result(v_rot)
    !! Rotate point `v` by `theta` [rad] about `axis` through `origin`
    !! (right-hand rule). Equivalent to Incompact3d's quaternion QuatRot,
    !! written in Rodrigues' form; `axis` need not be normalised.
    implicit none
    real(dp), intent(in) :: v(3), theta, axis(3), origin(3)
    real(dp) :: v_rot(3)

    real(dp) :: n(3), p(3)

    n = axis/norm2(axis)
    p = v - origin
    v_rot = origin + p*cos(theta) + cross(n, p)*sin(theta) &
            + n*dot_product(n, p)*(1._dp - cos(theta))
  end function quat_rot

  pure function cross(a, b) result(c)
    implicit none
    real(dp), intent(in) :: a(3), b(3)
    real(dp) :: c(3)

    c(1) = a(2)*b(3) - a(3)*b(2)
    c(2) = a(3)*b(1) - a(1)*b(3)
    c(3) = a(1)*b(2) - a(2)*b(1)
  end function cross

end module m_alm_element