---
# System prepended metadata

title: Fortran Serialization
tags: [cycle 14]

---

# Fortran Serialization

###### tags: `cycle 14`

- Developers:
- Appetite: full cycle

## Goal

Provide a flexible framework for serializing Fortran execution to improve testing and performance optimizations.

Currently, we see two cases were serialization would be handy:
- at the granule level (defined as standalone-usable Fortran components of ICON), which have a well-defined interface, but no serialization directives
- at the level where GT4Py stencils are integrated into ICON (via Liskov).


## Design

Enter a shared representation from which serialization statements can be generated for different kind of serialization libraries.

![](https://i.imgur.com/cW8apjG.png)


The shared representation can support different backends starting with `SerialBox`, and others such as `NetCDF`.


```mermaid
flowchart LR
    subgraph Frontend
    A[serialise.py] -->|f2py + regex| C{{Serialisation Interface}}
    B[Liskov] -->|Start/EndStencil Directives| C
    end
    subgraph Backend
    C --> D[SerialisationGenerator]
    D --> E[CodegenWriter]
    E --> F[F90File]
    end
```

To be able to target the same interface we effectively need **two** additional **deserialisers**. In Liskov, a `DeserialisedDirectives2SI` deserialiser, and in the Granule CLI a `ParsedGranule2SI` deserialiser. Both produce a `SerialisationInterface` object, which is passed to a shared `SerialisationGenerator`. This produces a list of `GeneratedCode` objects which is passed to the `SerialisationCodegenWriter` which writes it to the f90 file.


## Steps

1. `pp_ser` directives

- Refactor Christopher's script and add it to `pyutils`. Investigate use of `f2py` for parsing.

- Define serialisation interface to target generation of `pp_ser` directives.

- Liskov has already all the information needed to support generation of serialisation code, except metadata for a savepoint (e.g. timestep). In a first iteration this can be ignored (limiting to only one savepoint).

- Figure out a non-intrusive way to provide metadata for each stencil. 

- Write jinja templates for `pp_ser` directive generation as part of new subpackage in `Liskov`.

- Test build integration.

2. `serialbox` library calls.

- Investigate generation of serialbox library calls.
- If feasible, add new `serialbox` codegen backend to `Liskov`.
- Test build integration

3. `NetCDF` serialisation

- Investigate required NetCDF data structure to replicate Serialbox `savepoint`.
- If feasible, add new `netcdf` codegen backend to `Liskov`.


## Frontends

In both cases we need to get the information into the **Serialization Information** and then generate the place where to generate the serialization statements.

### Liskov directives

Stencil Information is generated from the Liskov directives + GT4Py stencil information.

Liskov directives in the code denote start and end of a stencil. 

- Line numbers of each Start and EndStencil.
- Stencil name is used to import information about stencil input/output fields, and dimensions.
- What key/value pairs should be included in the Savepoint information? Potentially use a timestep counter in the backend?

### Granule interface

Parses Fortran code to extract subroutine names, field intent etc.
- Needs to also record line numbers of where to generate code. Could use regex to find end of intent/out declarations and end of subroutine.
- Could f2py be used to parse the Fortran code?



## Christopher/Will script

This python (pre)serialization script, given an input Fortran90m source file, produces a new, modified version of the file itself (leaving the original unchanged), with Serialbox serialization directives in the needed places. The entire serialization procedure is based on two steps: 
1) the pre-serialization, performed with the script discussed here, which add the Serialbox serialization directives to the provided input source file. The output file is the input file for step 2. The script can be executed as follows:

./add_pp_ser_directives.py source.f90 -o pre_serialized_source.f90


2) the generation of the Fortran source file making use of Serialbox API, to be compiled and linked against Serialbox library. This second step is based on the usage of pp_ser.py, which takes in input the file produced in step 1.

During the parsing of the input file, all the function/subroutine parameters are stored together with their intent (in, out and inout, plus the function name in case it corresponds to the returned parameter). The intent are used to place the serialization directives in the right place: right before any computation for intent(in) parameter, right before any return statement for intent(out) parameter and in both places for intent(inout) parameter. Specific serialization names are used to identify in which point of the execution flow the serialization is performed. Special treatment is reserved to "jg" input parameter: if present, is considered to be a grid identifier and as such is used as part of the output folder name. 

Open issues:
1) How to deal with time loops? 
2) Composed types: Serialbox is currently not able to identify and serialize composed types (e.g., t_cartesian_coordinate). As a temporary solution, when such kind of variables are found we decompose them by creating new basic variable which are added to the Fortran source code and then serialized.
3) Arrays are serialized as scalar varibles since Serialbox should be able to deal with them. We need to find a way to load them with the right indexing scheme. Can we maybe serialize the indexing as well as a metadata?


## f2py parsing example

Example subroutine

```fortran!
subroutine mysubroutine(a, b, c)
    implicit none
    real, intent(in) :: a
    real, intent(inout) :: b
    real, intent(out) :: c
    c = a + b
    b = 2.0 * b
end subroutine mysubroutine
```

parse using f2py:

```python
from numpy.f2py.crackfortran import crackfortran

parsed = crackfortran('example.f90')
```

yields:

```python
[
    {
        "block": "subroutine",
        "name": "mysubroutine",
        "from": "example.f90",
        "args": ["a", "b", "c"],
        "body": [],
        "externals": [],
        "interfaced": [],
        "vars": {
            "a": {"typespec": "real", "attrspec": [], "intent": ["in"]},
            "b": {"typespec": "real", "attrspec": [], "intent": ["inout"]},
            "c": {"typespec": "real", "attrspec": [], "intent": ["out"]},
        },
        "entry": {},
        "implicit": None,
        "sortvars": ["a", "b", "c"],
    }
]

```

## Serialization Information

Interface for `pp_ser` code generation.

```python!
class Metadata:
    key: str
    value: str

class SerialisationMetaInfo:
    directory_path: str

class SavepointInformation:
    ln: int
    name: str
    fields: list[FieldSerializationInformation]
    metadata: Optional[list[Metadata]]

class FieldSerializationInformation:
    var_name: str
    var_association: str
```

Generation targets (`pp_ser`)

```bash
!$ser init directory=<path> prefix=<prefix>
!$ser mode write

!$ser savepoint <name> <**metadata>
!$ser <var_name>=<var_associaton>
```

Parsing and object construction pseudocode.

```python!
inp = SavePointInformation(name="in_fields", metadata={"jg", "jg"})
out = SavePointInformation(name="out_fields", metadata={"jg", "jg"})
for line in lines:
    if is_param(line):
        if is_intend_in_or_inout(line):
            inp.fields.append(get_info(line))
        if is_intend_out_or_inout(line):
            out.field.append(get_info(line))
```

Code generation context

```fortran!
subroutine mysubroutine(a, b, c)
    implicit none
    real, intent(in) :: a
    real, intent(inout) :: b
    real, intent(out) :: c
    
    <serialise_input_fields_here>
    
    c = a + b
    b = 2.0 * b

    <serialise_output_fields_here>
end subroutine mysubroutine
```




## Backend

Both front-ends should target the same code generation backend.

- Make inputs required for codegen use a common serialisation interface (`CodeGenInput`). 
- Inputs are passed to a `SerialisationGenerator` which generates codegen inputs.
- Passed to `SerialisationWriter`, which writes generated code to output file

### pp_ser

The first goal is to support generating `pp_ser` directives to serialise fields. Special care needs to be taken in the build integration so that liskov runs before `pp_ser`.

#### Directive examples

Serialbox provides `!$ser` preprocessor directives which when parsed by [`pp_ser.py`](https://github.com/GridTools/serialbox/blob/master/src/serialbox-python/pp_ser/pp_ser.py) generates calls to the Serialbox Fortran Interface, which in turn serialises data to disk. Required data points include:

- Field name
- Fortran field to serialise (e.g. `vct_a(:)`)
- Serialisation directory
- Prefix
- Savepoint metadata

**serialisation examples**


1.
    ```fortran!
    !$ser verbatim CALL datetimeToString(eventStartDate, dt_string)

    !$ser init directory='./ser_data' prefix='icon_pydycore' mpi_rank=get_my_mpi_work_id()

    !$ser verbatim DO jg=1, n_dom

    !$ser savepoint icon-grid nproma=nproma nlev=p_patch(jg)%nlev id=jg date=dstring nsteps=nsteps dtime=dtime n_dom=n_dom iforcing=iforcing lvert_nest=lvert_nest limited_area=l_limited_area

    !$ser mode write
    !$ser data vct_a=vct_a(:)
    !$ser data vct_b=vct_b(:)
    !$ser data nrdmax=nrdmax(jg)
    ```

2.
    ```fortran!
    MODULE m_ser

      IMPLICIT NONE

      CONTAINS

      SUBROUTINE serialize(a)
        IMPLICIT NONE
        REAL(KIND=8), DIMENSION(:,:,:) :: a

        !$ser init directory='.' prefix='SerialboxTest'
        !$ser savepoint sp1
        !$ser mode write
        !$ser data ser_a=a

      END SUBROUTINE serialize

      SUBROUTINE deserialize(a)
        IMPLICIT NONE
        REAL(KIND=8), DIMENSION(:,:,:) :: a

        !$ser init directory='.' prefix='SerialboxTest-output' prefix_ref='SerialboxTest'
        !$ser savepoint sp1
        !$ser mode read
        !$ser data ser_a=a
        !$ser mode write
        !$ser data ser_a=a

      END SUBROUTINE deserialize

      SUBROUTINE deserialize_with_perturb(a)
        IMPLICIT NONE
        REAL(KIND=8), DIMENSION(:,:,:) :: a
        REAL(KIND=8) :: rprecision
        rprecision = 10.0**(-PRECISION(1.0))

        !$ser init directory='.' prefix='SerialboxTest-output' prefix_ref='SerialboxTest' rprecision=rprecision rperturb=1.0e-5_8
        !$ser savepoint sp1
        !$ser mode read-perturb
        !$ser data ser_a=a

      END SUBROUTINE deserialize_with_perturb

    END MODULE m_ser
    ```

More examples can be seen at: https://github.com/C2SM/icon-exclaim/blob/strip_down_mo_nh_diffusion/src/atm_dyn_iconam/mo_nh_diffusion.f90

### optional: direct Serialbox library calls

The long-term solution is to generate serialbox calls directly, thereby circumventing pp_ser.

### optional: research if netCDF can be used for this use-case

To support NetCDF in the future it must be investigated whether additional metadata can be stored in NetCDF files, effectively replicating the `Savepoint` functionality of Serialbox, which is used to discriminate fields at different points in time. See https://github.com/GridTools/serialbox/blob/master/src/serialbox-python/serialbox/savepoint.py for more information.

Pseudocode representation of a savepoint is as follows:


```python
{
    ("stencil1", "savepoint1"): {"vn": vn, "foo": bar}, 
    ("stencil1", "savepoint2"): {"vn": vn, "foo": bar}
}
```

#### Simple NetCDF Serialisation Example

For serialisation to NetCDF the following information is needed:

- Array dimensions (A name for each dimension)
- Name of the array to serialise


```fortran!
program serialize_to_netcdf
  use netcdf
  implicit none

  integer, parameter :: nx = 10, ny = 5
  integer :: status, ncid, varid
  integer :: data(nx,ny)
  integer :: i, j

  ! initialize data
  do i = 1, nx
    do j = 1, ny
      data(i,j) = (i-1)*ny + j
    end do
  end do

  ! create NetCDF file
  status = nf90_create("data.nc", NF90_CLOBBER, ncid)
  if (status /= NF90_NOERR) then
    write(*,*) "Error creating NetCDF file"
    stop
  end if

  ! define dimensions
  status = nf90_def_dim(ncid, "x", nx, varid)
  if (status /= NF90_NOERR) then
    write(*,*) "Error defining dimension 'x'"
    stop
  end if
  status = nf90_def_dim(ncid, "y", ny, varid)
  if (status /= NF90_NOERR) then
    write(*,*) "Error defining dimension 'y'"
    stop
  end if

  ! define variable
  status = nf90_def_var(ncid, "data", NF90_INT, (/nf90_dimid(ncid, "x"), nf90_dimid(ncid, "y")/), varid)
  if (status /= NF90_NOERR) then
    write(*,*) "Error defining variable 'data'"
    stop
  end if

  ! end definition mode
  status = nf90_enddef(ncid)
  if (status /= NF90_NOERR) then
    write(*,*) "Error ending definition mode"
    stop
  end if

  ! write data
  status = nf90_put_var(ncid, nf90_varid(ncid, "data"), data)
  if (status /= NF90_NOERR) then
    write(*,*) "Error writing variable 'data'"
    stop
  end if

  ! close file
  status = nf90_close(ncid)
  if (status /= NF90_NOERR) then
    write(*,*) "Error closing NetCDF file"
    stop
  end if

  write(*,*) "Successfully serialized Fortran array to NetCDF file 'data.nc'"
end program serialize_to_netcdf
```

## Rabbit holes

- For serialization Liskov can use GT4Py stencil information, no additional directives will be added (except potentially metadata).

## No-gos

- Do not extend Liskov's parser to also parse Fortran code, as this would further increase the complexity/maintainability of Liskov which is meant to be a simple code generation tool.


## Current Status

- `f2ser` command line script done. Parses f90 granule and generates corresponding `pp_ser` statements.

- Serialisation statement generation now an option in `icon_liskov`.
    - Can serialise `mo_velocity_advection`, `mo_nh_diffusion`

- Tested integration into ICON build system.


## Todo
- Support serialisation for `mo_solve_nonhydro`, and other stencils defined in subroutines such as `mo_int_rbf`

- Code cleanup

- Unit/Integration Testing

