aradi / fypp

Python powered Fortran preprocessor
http://fypp.readthedocs.io
BSD 2-Clause "Simplified" License
180 stars 30 forks source link

Accessing Python dictionaries within fypp #20

Closed wyphan closed 2 years ago

wyphan commented 2 years ago

Is it possible to assign and retrieve Python dictionary key-value pairs from within fypp without writing a dedicated Python class? If yes, what is the correct syntax to do so?

wyphan commented 2 years ago

The specific use case I'm aiming for is a testing framework. I want to define a macro that assigns the "golden" value into the Python dictionary key-value pair, and another macro that checks the test result against the "golden" value. I want to use Python dictionaries because the value can be a Python list (which can then be mapped into a Fortran 1-D array).

aradi commented 2 years ago

You can use the .update() in an Python expression to update values in a dictionary:

#:set values = {"energy": 12.0, "force": [1.0, 0.0, -1.0]}
$:values["energy"]
$:values.update([("energy", 100.0)])
$:values["energy"]
wyphan commented 2 years ago

Thanks for the quick response! I think the next problem is the scoping of the Python dictionary. How can I declare it as a global variable so it spans multiple macros in the same fypp input file?

For instance, here's what I've put up so far as mod_testing.fypp:

#! Macro to initialize testing infrastructure
#:def test_init()
  #! Number of tests and errors
  #:global ntst
  #:set ntst = 0
  #:global nerr
  #:set nerr = 0
  #! Dictionaries to hold golden values and test labels
  #:global chkval
  #:set chkval = {}
  #:global tstlbl
  #:set tstlbl = {}
#:enddef

#! Macro to register golden value
#:def save_gold( lbl, val )
  $:tstlbl.update([(ntst, lbl)])
  $:chkval.update([(ntst, val)])
#:enddef save_gold

#! Macro to check scalar test result exactly against golden value
#:def check_exact( val )
  $:lbl = tstlbl[ntst]
  $:gold = chkval[ntst]
  BLOCK
    USE ISO_FORTRAN_ENV, ONLY: o => output_unit, e => error_unit
    WRITE(o,'(A)') ${lbl}$
  #:if val == gold
    WRITE(e,'("OK: result = ${val}$")')
  #:else
    $:nerr += 1
    WRITE(e,'("Error[${_FILE_}$]: result = ${val}$, should be ${gold}")')
  #:endif
  END BLOCK
  $:ntst += 1
#:enddef check_exact

And then here's where it's used, in test_gamma.fypp:

#:include "mod_testing.fypp"
!===============================================================================
! Unit test for "Gamma" offset functions
! Last edited: Oct 26, 2021 (WYP)
!===============================================================================
PROGRAM testgamma

  USE ISO_FORTRAN_ENV, ONLY: u => output_unit
  ! USE moa
  IMPLICIT NONE

  !-----------------------------------------------------------------------------
  ! Test for empty shape vector
  !-----------------------------------------------------------------------------

  WRITE(u,'("Test for empty shape")')

  @:save_gold( "Gamma( <>, <> )", 0 )
  ! r = moa_gamma( [], [] )
  @:check_exact( r )
  WRITE(u,*)

  STOP
END PROGRAM testgamma

The error message that I got from fypp when preprocessing test_gamma.fypp is:

test_gamma.fypp:13: error: exception occurred when calling 'save_gold' [FyppFatalError]
mod_testing.fypp:17: error: exception occurred when evaluating 'tstlbl.update([(ntst, lbl)])' [FyppFatalError]
error: name 'tstlbl' is not defined [NameError]
wyphan commented 2 years ago

Never mind, I figured it out! Here's my current mod_testing_macros.fypp:

#! -*- mode: f90; -*-
#:mute
!===============================================================================
! Testing infrastructure fypp macros
! Last edited: Nov 10, 2021 (WYP)
!===============================================================================
#:endmute

#! Initialize testing infrastructure

#! Number of tests
#:global ntst
#:set ntst = 0

#! Dictionaries to hold golden values and test labels
#:global chkval
#:set chkval = {}
#:global tstlbl
#:set tstlbl = {}

#! Macro to register golden value
#:def save_gold( lbl, val )
  $:tstlbl.update([(ntst, lbl)])
  $:chkval.update([(ntst, val)])
#:enddef save_gold

#! Macro to check scalar test result exactly against golden value
#:def check_exact( val )
  #:set lbl  = tstlbl[ntst]
  #:set gold = chkval[ntst]
  BLOCK
    USE ISO_FORTRAN_ENV, ONLY: o => output_unit, e => error_unit
    WRITE(o,'(A)') ${lbl}$
    IF( ${val}$ == ${gold}$ ) THEN
      WRITE(e,*) 'OK: result = ', ${val}$
    ELSE
      nerr = nerr + 1
      WRITE(e,*) 'Error[${_FILE_}$]: result = ', ${val}$, ', should be ${gold}$'
    END IF
  END BLOCK
  #:set ntst = ntst + 1
#:enddef check_exact

#:def test_summary()
  BLOCK
    USE ISO_FORTRAN_ENV, ONLY: o => output_unit
    WRITE(o,'("Summary for ${_FILE_}$:")')
    IF( nerr > 0 ) THEN
      WRITE(o,'("  ",I0," out of ${ntst}$ tests failed")') nerr
    ELSE
      WRITE(o,'("  All ${ntst}$ tests succeeded")')
    END IF
  END BLOCK
#:enddef test_summary
aradi commented 2 years ago

I am happy you managed. Just a comment: Variables in the global scope (chkval, tslb1, etc.) do not need a #:global assignment, they are global by default...

Otherwise, you may eventually be interested in the FyTest unit testing framework, which offers unit testing and uses the aforementioned dictionary manipulation quite heavily.