# fortran Integration

This example demonstrates how to use fluids from fortran.

## Source Code

      1module python_utils
      2  use iso_c_binding
      3  implicit none
      4
      5  ! Python object handles
      6  type(c_ptr) :: py_module = c_null_ptr
      7  type(c_ptr) :: py_none = c_null_ptr
      8
      9contains
     10  subroutine init_python()
     11    interface
     12      subroutine Py_Initialize() bind(c, name='Py_Initialize')
     13      end subroutine
     14
     15      function PyImport_ImportModule(name) bind(c, name='PyImport_ImportModule')
     16        use iso_c_binding
     17        character(kind=c_char), dimension(*) :: name
     18        type(c_ptr) :: PyImport_ImportModule
     19      end function
     20    end interface
     21
     22    ! Initialize Python
     23    call Py_Initialize()
     24
     25    ! Import fluids module
     26    py_module = PyImport_ImportModule('fluids'//c_null_char)
     27    if (.not. c_associated(py_module)) then
     28      print *, 'Failed to import fluids module'
     29      call exit(1)
     30    end if
     31  end subroutine
     32
     33  subroutine test_fluids()
     34    interface
     35      function PyObject_GetAttrString(obj, name) bind(c, name='PyObject_GetAttrString')
     36        use iso_c_binding
     37        type(c_ptr), value :: obj
     38        character(kind=c_char), dimension(*) :: name
     39        type(c_ptr) :: PyObject_GetAttrString
     40      end function
     41
     42      function PyFloat_AsDouble(obj) bind(c, name='PyFloat_AsDouble')
     43        use iso_c_binding
     44        type(c_ptr), value :: obj
     45        real(c_double) :: PyFloat_AsDouble
     46      end function
     47
     48      function PyUnicode_AsUTF8(obj) bind(c, name='PyUnicode_AsUTF8')
     49        use iso_c_binding
     50        type(c_ptr), value :: obj
     51        type(c_ptr) :: PyUnicode_AsUTF8
     52      end function
     53
     54      function PyTuple_New(size) bind(c, name='PyTuple_New')
     55        use iso_c_binding
     56        integer(c_size_t), value :: size
     57        type(c_ptr) :: PyTuple_New
     58      end function
     59
     60      function PyDict_New() bind(c, name='PyDict_New')
     61        use iso_c_binding
     62        type(c_ptr) :: PyDict_New
     63      end function
     64
     65      function PyFloat_FromDouble(val) bind(c, name='PyFloat_FromDouble')
     66        use iso_c_binding
     67        real(c_double), value :: val
     68        type(c_ptr) :: PyFloat_FromDouble
     69      end function
     70
     71      subroutine PyDict_SetItemString(dict, key, val) bind(c, name='PyDict_SetItemString')
     72        use iso_c_binding
     73        type(c_ptr), value :: dict
     74        character(kind=c_char), dimension(*) :: key
     75        type(c_ptr), value :: val
     76      end subroutine
     77
     78      function PyObject_Call(callable, args, kwargs) bind(c, name='PyObject_Call')
     79        use iso_c_binding
     80        type(c_ptr), value :: callable
     81        type(c_ptr), value :: args
     82        type(c_ptr), value :: kwargs
     83        type(c_ptr) :: PyObject_Call
     84      end function
     85    end interface
     86
     87    type(c_ptr) :: version, reynolds_func, args, kwargs, result, str_ptr
     88    real(c_double) :: re
     89    character(len=100), pointer :: version_str
     90    character(kind=c_char), pointer :: c_str(:)
     91    character(len=100) :: temp_str
     92    integer :: i
     93
     94    print *, 'Running fluids tests from Fortran...'
     95
     96    ! Get version
     97    version = PyObject_GetAttrString(py_module, '__version__'//c_null_char)
     98    if (c_associated(version)) then
     99      str_ptr = PyUnicode_AsUTF8(version)
    100      call c_f_pointer(str_ptr, c_str, [100])
    101      temp_str = ''
    102      i = 1
    103      do while (c_str(i) /= c_null_char .and. i <= 100)
    104        temp_str(i:i) = c_str(i)
    105        i = i + 1
    106      end do
    107
    108      print *, '✓ Successfully imported fluids'
    109      print *, '✓ Fluids version: ', trim(temp_str)
    110    end if
    111    ! Test Reynolds number calculation
    112    reynolds_func = PyObject_GetAttrString(py_module, 'Reynolds'//c_null_char)
    113    if (c_associated(reynolds_func)) then
    114      args = PyTuple_New(0_c_size_t)
    115      kwargs = PyDict_New()
    116
    117      call PyDict_SetItemString(kwargs, 'V'//c_null_char, PyFloat_FromDouble(2.5d0))
    118      call PyDict_SetItemString(kwargs, 'D'//c_null_char, PyFloat_FromDouble(0.1d0))
    119      call PyDict_SetItemString(kwargs, 'rho'//c_null_char, PyFloat_FromDouble(1000.0d0))
    120      call PyDict_SetItemString(kwargs, 'mu'//c_null_char, PyFloat_FromDouble(0.001d0))
    121
    122      result = PyObject_Call(reynolds_func, args, kwargs)
    123      if (c_associated(result)) then
    124        re = PyFloat_AsDouble(result)
    125        print *, '✓ Reynolds number calculation successful: ', re
    126        if (re > 0) then
    127          print *, 'Assert passed: Re > 0'
    128        else
    129          print *, 'Assert failed: Re <= 0'
    130        end if
    131      end if
    132    end if
    133  end subroutine
    134
    135  subroutine benchmark_fluids()
    136    interface
    137      function PyObject_GetAttrString(obj, name) bind(c, name='PyObject_GetAttrString')
    138        use iso_c_binding
    139        type(c_ptr), value :: obj
    140        character(kind=c_char), dimension(*) :: name
    141        type(c_ptr) :: PyObject_GetAttrString
    142      end function
    143
    144      function PyTuple_New(size) bind(c, name='PyTuple_New')
    145        use iso_c_binding
    146        integer(c_size_t), value :: size
    147        type(c_ptr) :: PyTuple_New
    148      end function
    149
    150      function PyDict_New() bind(c, name='PyDict_New')
    151        use iso_c_binding
    152        type(c_ptr) :: PyDict_New
    153      end function
    154
    155      function PyFloat_FromDouble(val) bind(c, name='PyFloat_FromDouble')
    156        use iso_c_binding
    157        real(c_double), value :: val
    158        type(c_ptr) :: PyFloat_FromDouble
    159      end function
    160
    161      subroutine PyDict_SetItemString(dict, key, val) bind(c, name='PyDict_SetItemString')
    162        use iso_c_binding
    163        type(c_ptr), value :: dict
    164        character(kind=c_char), dimension(*) :: key
    165        type(c_ptr), value :: val
    166      end subroutine
    167
    168      function PyObject_Call(callable, args, kwargs) bind(c, name='PyObject_Call')
    169        use iso_c_binding
    170        type(c_ptr), value :: callable
    171        type(c_ptr), value :: args
    172        type(c_ptr), value :: kwargs
    173        type(c_ptr) :: PyObject_Call
    174      end function
    175    end interface
    176
    177    type(c_ptr) :: friction_func
    178    type(c_ptr) :: args, kwargs, result
    179    integer :: i
    180    real(c_double) :: start_time, end_time
    181
    182    print *, 'Running benchmarks:'
    183
    184    ! Get friction_factor function
    185    friction_func = PyObject_GetAttrString(py_module, 'friction_factor'//c_null_char)
    186    if (.not. c_associated(friction_func)) then
    187      print *, 'Failed to get friction_factor function'
    188      return
    189    end if
    190
    191    ! Benchmark friction_factor
    192    print *, 'Benchmarking friction_factor:'
    193    call cpu_time(start_time)
    194
    195    do i = 1, 1000000
    196      args = PyTuple_New(0_c_size_t)
    197      kwargs = PyDict_New()
    198
    199      call PyDict_SetItemString(kwargs, 'Re'//c_null_char, PyFloat_FromDouble(1.0d5))
    200      call PyDict_SetItemString(kwargs, 'eD'//c_null_char, PyFloat_FromDouble(0.0001d0))
    201
    202      result = PyObject_Call(friction_func, args, kwargs)
    203    end do
    204
    205    call cpu_time(end_time)
    206    print *, 'Time for 1e6 friction_factor calls: ', end_time - start_time, ' seconds'
    207    print *, 'Average time per call: ', (end_time - start_time) / 1000000.0, ' seconds'
    208  end subroutine
    209
    210
    211  subroutine test_tank()
    212    interface
    213      function PyObject_GetAttrString(obj, name) bind(c, name='PyObject_GetAttrString')
    214        use iso_c_binding
    215        type(c_ptr), value :: obj
    216        character(kind=c_char), dimension(*) :: name
    217        type(c_ptr) :: PyObject_GetAttrString
    218      end function
    219
    220      function PyTuple_New(size) bind(c, name='PyTuple_New')
    221        use iso_c_binding
    222        integer(c_size_t), value :: size
    223        type(c_ptr) :: PyTuple_New
    224      end function
    225
    226      function PyDict_New() bind(c, name='PyDict_New')
    227        use iso_c_binding
    228        type(c_ptr) :: PyDict_New
    229      end function
    230
    231      function PyFloat_FromDouble(val) bind(c, name='PyFloat_FromDouble')
    232        use iso_c_binding
    233        real(c_double), value :: val
    234        type(c_ptr) :: PyFloat_FromDouble
    235      end function
    236
    237      function PyBool_FromLong(val) bind(c, name='PyBool_FromLong')
    238        use iso_c_binding
    239        integer(c_long), value :: val
    240        type(c_ptr) :: PyBool_FromLong
    241      end function
    242
    243      function PyFloat_AsDouble(obj) bind(c, name='PyFloat_AsDouble')
    244        use iso_c_binding
    245        type(c_ptr), value :: obj
    246        real(c_double) :: PyFloat_AsDouble
    247      end function
    248
    249      function PyUnicode_FromString(str) bind(c, name='PyUnicode_FromString')
    250        use iso_c_binding
    251        character(kind=c_char), dimension(*) :: str
    252        type(c_ptr) :: PyUnicode_FromString
    253      end function
    254
    255      subroutine PyDict_SetItemString(dict, key, val) bind(c, name='PyDict_SetItemString')
    256        use iso_c_binding
    257        type(c_ptr), value :: dict
    258        character(kind=c_char), dimension(*) :: key
    259        type(c_ptr), value :: val
    260      end subroutine
    261
    262      function PyObject_Call(callable, args, kwargs) bind(c, name='PyObject_Call')
    263        use iso_c_binding
    264        type(c_ptr), value :: callable
    265        type(c_ptr), value :: args
    266        type(c_ptr), value :: kwargs
    267        type(c_ptr) :: PyObject_Call
    268      end function
    269
    270      subroutine PyTuple_SetItem(tup, pos, item) bind(c, name='PyTuple_SetItem')
    271        use iso_c_binding
    272        type(c_ptr), value :: tup
    273        integer(c_size_t), value :: pos
    274        type(c_ptr), value :: item
    275      end subroutine
    276
    277      function PyUnicode_AsUTF8(obj) bind(c, name='PyUnicode_AsUTF8')
    278        use iso_c_binding
    279        type(c_ptr), value :: obj
    280        type(c_ptr) :: PyUnicode_AsUTF8
    281      end function
    282
    283      function PyObject_CallMethod(obj, method, format) bind(c, name='PyObject_CallMethod')
    284        use iso_c_binding
    285        type(c_ptr), value :: obj
    286        character(kind=c_char), dimension(*) :: method
    287        character(kind=c_char), dimension(*) :: format
    288        type(c_ptr) :: PyObject_CallMethod
    289      end function
    290
    291      function PyObject_Str(obj) bind(c, name='PyObject_Str')
    292        use iso_c_binding
    293        type(c_ptr), value :: obj
    294        type(c_ptr) :: PyObject_Str
    295      end function
    296
    297      subroutine Py_DecRef(obj) bind(c, name='Py_DecRef')
    298        use iso_c_binding
    299        type(c_ptr), value :: obj
    300      end subroutine
    301
    302    end interface
    303
    304    type(c_ptr) :: tank_class, args1, kwargs1, T1, length, diameter
    305    type(c_ptr) :: tank_ellip, args_ellip, kwargs_ellip, ellip_length
    306    type(c_ptr) :: args2, kwargs2, DIN, h_max, tank_str
    307    type(c_ptr) :: h_from_V, V_from_h, SA_from_h
    308    type(c_ptr) :: arg40, arg41, arg21, result40, result41, result21
    309    real(c_double) :: tank_length, tank_diameter, ellip_l, max_height
    310    real(c_double) :: height_40, volume_41, surface_area_21
    311    character(kind=c_char), pointer :: c_str(:)
    312    character(len=1000) :: temp_str
    313    integer :: i
    314    type(c_ptr) :: str_ptr
    315
    316    print *, 'Testing tank calculations:'
    317
    318    ! Get TANK class
    319    tank_class = PyObject_GetAttrString(py_module, 'TANK'//c_null_char)
    320    if (.not. c_associated(tank_class)) then
    321      print *, 'Failed to get TANK class'
    322      return
    323    end if
    324
    325    ! Test basic tank creation
    326    args1 = PyTuple_New(0_c_size_t)
    327    kwargs1 = PyDict_New()
    328
    329    call PyDict_SetItemString(kwargs1, 'V'//c_null_char, PyFloat_FromDouble(10.0d0))
    330    call PyDict_SetItemString(kwargs1, 'L_over_D'//c_null_char, PyFloat_FromDouble(0.7d0))
    331    call PyDict_SetItemString(kwargs1, 'sideB'//c_null_char, PyUnicode_FromString('conical'//c_null_char))
    332    call PyDict_SetItemString(kwargs1, 'horizontal'//c_null_char, PyBool_FromLong(0_c_long))
    333
    334    T1 = PyObject_Call(tank_class, args1, kwargs1)
    335    if (c_associated(T1)) then
    336      length = PyObject_GetAttrString(T1, 'L'//c_null_char)
    337      diameter = PyObject_GetAttrString(T1, 'D'//c_null_char)
    338
    339      if (c_associated(length) .and. c_associated(diameter)) then
    340        tank_length = PyFloat_AsDouble(length)
    341        tank_diameter = PyFloat_AsDouble(diameter)
    342        print *, '✓ Tank length: ', tank_length
    343        print *, '✓ Tank diameter: ', tank_diameter
    344      end if
    345    end if
    346
    347    ! Test ellipsoidal tank
    348    args_ellip = PyTuple_New(0_c_size_t)
    349    kwargs_ellip = PyDict_New()
    350
    351    call PyDict_SetItemString(kwargs_ellip, 'D'//c_null_char, PyFloat_FromDouble(10.0d0))
    352    call PyDict_SetItemString(kwargs_ellip, 'V'//c_null_char, PyFloat_FromDouble(500.0d0))
    353    call PyDict_SetItemString(kwargs_ellip, 'horizontal'//c_null_char, PyBool_FromLong(0_c_long))
    354    call PyDict_SetItemString(kwargs_ellip, 'sideA'//c_null_char, PyUnicode_FromString('ellipsoidal'//c_null_char))
    355    call PyDict_SetItemString(kwargs_ellip, 'sideB'//c_null_char, PyUnicode_FromString('ellipsoidal'//c_null_char))
    356    call PyDict_SetItemString(kwargs_ellip, 'sideA_a'//c_null_char, PyFloat_FromDouble(1.0d0))
    357    call PyDict_SetItemString(kwargs_ellip, 'sideB_a'//c_null_char, PyFloat_FromDouble(1.0d0))
    358
    359    tank_ellip = PyObject_Call(tank_class, args_ellip, kwargs_ellip)
    360    if (c_associated(tank_ellip)) then
    361      ellip_length = PyObject_GetAttrString(tank_ellip, 'L'//c_null_char)
    362      if (c_associated(ellip_length)) then
    363        ellip_l = PyFloat_AsDouble(ellip_length)
    364        print *, '✓ Ellipsoidal tank L: ', ellip_l
    365      end if
    366    end if
    367
    368    ! Test torispherical tank
    369    args2 = PyTuple_New(0_c_size_t)
    370    kwargs2 = PyDict_New()
    371
    372    call PyDict_SetItemString(kwargs2, 'L'//c_null_char, PyFloat_FromDouble(3.0d0))
    373    call PyDict_SetItemString(kwargs2, 'D'//c_null_char, PyFloat_FromDouble(5.0d0))
    374    call PyDict_SetItemString(kwargs2, 'horizontal'//c_null_char, PyBool_FromLong(0_c_long))
    375    call PyDict_SetItemString(kwargs2, 'sideA'//c_null_char, PyUnicode_FromString('torispherical'//c_null_char))
    376    call PyDict_SetItemString(kwargs2, 'sideB'//c_null_char, PyUnicode_FromString('torispherical'//c_null_char))
    377    call PyDict_SetItemString(kwargs2, 'sideA_f'//c_null_char, PyFloat_FromDouble(1.0d0))
    378    call PyDict_SetItemString(kwargs2, 'sideA_k'//c_null_char, PyFloat_FromDouble(0.1d0))
    379    call PyDict_SetItemString(kwargs2, 'sideB_f'//c_null_char, PyFloat_FromDouble(1.0d0))
    380    call PyDict_SetItemString(kwargs2, 'sideB_k'//c_null_char, PyFloat_FromDouble(0.1d0))
    381
    382    DIN = PyObject_Call(tank_class, args2, kwargs2)
    383    if (c_associated(DIN)) then
    384      ! Get string representation
    385      tank_str = PyObject_Str(DIN)
    386      if (c_associated(tank_str)) then
    387        str_ptr = PyUnicode_AsUTF8(tank_str)
    388        if (c_associated(str_ptr)) then
    389          call c_f_pointer(str_ptr, c_str, [1000])
    390          temp_str = ''
    391          i = 1
    392          do while (c_str(i) /= c_null_char .and. i <= 1000)
    393            temp_str(i:i) = c_str(i)
    394            i = i + 1
    395          end do
    396          print *, '✓ Tank representation: ', trim(temp_str)
    397        end if
    398      end if
    399
    400      ! Get max height
    401      h_max = PyObject_GetAttrString(DIN, 'h_max'//c_null_char)
    402      if (c_associated(h_max)) then
    403        max_height = PyFloat_AsDouble(h_max)
    404        print *, '✓ Tank max height: ', max_height
    405      end if
    406
    407      ! Test h_from_V method
    408      h_from_V = PyObject_GetAttrString(DIN, 'h_from_V'//c_null_char)
    409      if (c_associated(h_from_V)) then
    410        ! Create tuple argument
    411        arg40 = PyTuple_New(1_c_size_t)
    412        ! Set tuple item directly
    413        call PyTuple_SetItem(arg40, 0_c_size_t, PyFloat_FromDouble(40.0d0))
    414        ! Call method
    415        result40 = PyObject_Call(h_from_V, arg40, c_null_ptr)
    416        if (c_associated(result40)) then
    417          height_40 = PyFloat_AsDouble(result40)
    418          print *, '✓ Height at V=40: ', height_40
    419
    420          ! Cleanup
    421          if (c_associated(result40)) then
    422            call Py_DecRef(result40)
    423          end if
    424        end if
    425
    426        ! Cleanup
    427        if (c_associated(h_from_V)) then
    428          call Py_DecRef(h_from_V)
    429        end if
    430        if (c_associated(arg40)) then
    431          call Py_DecRef(arg40)
    432        end if
    433      end if
    434
    435
    436
    437      ! Test SA_from_h method
    438      SA_from_h = PyObject_GetAttrString(DIN, 'SA_from_h'//c_null_char)
    439      if (c_associated(SA_from_h)) then
    440        ! Create tuple argument
    441        arg21 = PyTuple_New(1_c_size_t)
    442        ! Set tuple item directly
    443        call PyTuple_SetItem(arg21, 0_c_size_t, PyFloat_FromDouble(2.1d0))
    444        ! Call method
    445        result21 = PyObject_Call(SA_from_h, arg21, c_null_ptr)
    446        if (c_associated(result21)) then
    447          surface_area_21 = PyFloat_AsDouble(result21)
    448          print *, '✓ Surface area at h=2.1: ', surface_area_21
    449
    450          ! Cleanup
    451          if (c_associated(result21)) then
    452            call Py_DecRef(result21)
    453          end if
    454        end if
    455
    456        ! Cleanup
    457        if (c_associated(SA_from_h)) then
    458          call Py_DecRef(SA_from_h)
    459        end if
    460        if (c_associated(arg21)) then
    461          call Py_DecRef(arg21)
    462        end if
    463      end if
    464    end if
    465
    466  end subroutine test_tank
    467  ! Helper function to convert C string pointer to Fortran string
    468  subroutine c_f_string_ptr(c_str, f_str)
    469    type(c_ptr), intent(in) :: c_str
    470    character(len=*), pointer, intent(out) :: f_str
    471
    472    character(kind=c_char), pointer :: tmp(:)
    473    integer :: i, n
    474    character(len=1000) :: temp_str  ! Temporary buffer
    475
    476    call c_f_pointer(c_str, tmp, [1000])  ! Limited to reasonable size
    477    n = 0
    478    do while (tmp(n+1) /= c_null_char .and. n < 1000)
    479      n = n + 1
    480      temp_str(n:n) = tmp(n)
    481    end do
    482
    483    allocate(character(len=n) :: f_str)
    484    f_str = temp_str(1:n)
    485  end subroutine
    486
    487subroutine test_reynolds()
    488    use iso_c_binding
    489    implicit none
    490
    491    interface
    492      function PyObject_GetAttrString(obj, name) bind(c, name='PyObject_GetAttrString')
    493        import :: c_ptr, c_char
    494        type(c_ptr), value :: obj
    495        character(kind=c_char), dimension(*) :: name
    496        type(c_ptr) :: PyObject_GetAttrString
    497      end function
    498
    499      function PyTuple_New(size) bind(c, name='PyTuple_New')
    500        import :: c_ptr, c_size_t
    501        integer(c_size_t), value :: size
    502        type(c_ptr) :: PyTuple_New
    503      end function
    504
    505      function PyDict_New() bind(c, name='PyDict_New')
    506        import :: c_ptr
    507        type(c_ptr) :: PyDict_New
    508      end function
    509
    510      function PyFloat_FromDouble(val) bind(c, name='PyFloat_FromDouble')
    511        import :: c_ptr, c_double
    512        real(c_double), value :: val
    513        type(c_ptr) :: PyFloat_FromDouble
    514      end function
    515
    516      function PyFloat_AsDouble(obj) bind(c, name='PyFloat_AsDouble')
    517        import :: c_ptr, c_double
    518        type(c_ptr), value :: obj
    519        real(c_double) :: PyFloat_AsDouble
    520      end function
    521
    522      subroutine PyDict_SetItemString(dict, key, val) bind(c, name='PyDict_SetItemString')
    523        import :: c_ptr, c_char
    524        type(c_ptr), value :: dict
    525        character(kind=c_char), dimension(*) :: key
    526        type(c_ptr), value :: val
    527      end subroutine
    528
    529      function PyObject_Call(callable, args, kwargs) bind(c, name='PyObject_Call')
    530        import :: c_ptr
    531        type(c_ptr), value :: callable
    532        type(c_ptr), value :: args
    533        type(c_ptr), value :: kwargs
    534        type(c_ptr) :: PyObject_Call
    535      end function
    536    end interface
    537
    538    type(c_ptr) :: reynolds_func, args, kwargs, result
    539    real(c_double) :: Re1, Re2
    540    logical :: test_passed
    541
    542    print *, 'Testing Reynolds number calculations:'
    543
    544    ! Get Reynolds function
    545    reynolds_func = PyObject_GetAttrString(py_module, "Reynolds"//C_NULL_CHAR)
    546    if (.not. c_associated(reynolds_func)) then
    547      print *, 'Failed to get Reynolds function'
    548      return
    549    end if
    550
    551    ! Test with density and viscosity
    552    args = PyTuple_New(0_c_size_t)
    553    kwargs = PyDict_New()
    554
    555    call PyDict_SetItemString(kwargs, "V"//C_NULL_CHAR, PyFloat_FromDouble(2.5d0))
    556    call PyDict_SetItemString(kwargs, "D"//C_NULL_CHAR, PyFloat_FromDouble(0.25d0))
    557    call PyDict_SetItemString(kwargs, "rho"//C_NULL_CHAR, PyFloat_FromDouble(1.1613d0))
    558    call PyDict_SetItemString(kwargs, "mu"//C_NULL_CHAR, PyFloat_FromDouble(1.9d-5))
    559
    560    result = PyObject_Call(reynolds_func, args, kwargs)
    561    if (c_associated(result)) then
    562      Re1 = PyFloat_AsDouble(result)
    563      print *, '✓ Re (with rho, mu): ', Re1
    564
    565      ! Assert abs(Re1 - 38200.6579) < 0.1
    566      test_passed = abs(Re1 - 38200.6579d0) < 0.1d0
    567      if (test_passed) then
    568        print *, 'Assert passed: |Re1 - 38200.6579| < 0.1'
    569      else
    570        print *, 'Assert failed: |Re1 - 38200.6579| < 0.1'
    571      end if
    572    end if
    573
    574    ! Test with kinematic viscosity
    575    args = PyTuple_New(0_c_size_t)
    576    kwargs = PyDict_New()
    577
    578    call PyDict_SetItemString(kwargs, "V"//C_NULL_CHAR, PyFloat_FromDouble(2.5d0))
    579    call PyDict_SetItemString(kwargs, "D"//C_NULL_CHAR, PyFloat_FromDouble(0.25d0))
    580    call PyDict_SetItemString(kwargs, "nu"//C_NULL_CHAR, PyFloat_FromDouble(1.636d-5))
    581
    582    result = PyObject_Call(reynolds_func, args, kwargs)
    583    if (c_associated(result)) then
    584      Re2 = PyFloat_AsDouble(result)
    585      print *, '✓ Re (with nu): ', Re2
    586
    587      ! Assert abs(Re2 - 38202.934) < 0.1
    588      test_passed = abs(Re2 - 38202.934d0) < 0.1d0
    589      if (test_passed) then
    590        print *, 'Assert passed: |Re2 - 38202.934| < 0.1'
    591      else
    592        print *, 'Assert failed: |Re2 - 38202.934| < 0.1'
    593      end if
    594    end if
    595
    596  end subroutine test_reynolds
    597end module
    598
    599program main
    600  use python_utils
    601  implicit none
    602
    603  call init_python()
    604  call test_fluids()
    605  call test_tank()
    606  call test_reynolds()
    607  call benchmark_fluids()
    608
    609  print *, 'All tests completed!'
    610end program main

## Requirements

- Python with fluids installed

## Usage Notes

- The example is incomplete. iso_c_binding is used to interface without actually writing C code.

- The speed is comparable to pybind11, 2 microseconds per friction factor call.

- The excess amount of code required to interface the two languages is very significant