Skip to content

Commit

Permalink
Python/Fortran interface generator: bases (#27)
Browse files Browse the repository at this point in the history
* Generate bridge and basic hook

* Move to jinja2 template generator

* Fix setup for template deploy and new dependencies

* Copy support file
Hook/Py interface use an object for re-entry

* Sanitize variable name for Python
Add partial cmake example
Remove forced include to keep interface minimal (files are still copied)
Copy header twice so it's present for both lib link and source compile

* Fixes

* Read yaml data as class: Argument
Cleaner data_conversion
Test

* Validation: generate a validator call
Clean data_conversion for direct usage
Refactor: break code in smaller files & clean up

* Python interface unit test
Add __init__.py in the interface for python guidelines

* Automatic array translation for validation
  • Loading branch information
FlorianDeconinck authored Dec 15, 2023
1 parent bbd629d commit 6ab929f
Show file tree
Hide file tree
Showing 22 changed files with 1,915 additions and 0 deletions.
22 changes: 22 additions & 0 deletions geosongpu_ci/tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Python <-> Fortan interface generator

## Porting validation generator

Generates a call that will compare the outputs of the reference and the ported code.

WARNING: the generator work on the hypothesis that the fortran<>python interface and the reference fortran share the argument signature.

Yaml schema:

```yaml
bridge:
- name: function_that_will_be_validated
arguments:
inputs: ...
inouts: ...
outputs: ...
validation:
reference:
call: fortran_routine_name
mod: fortran_module_name_to_use_to_get_to_call
```
Empty file.
97 changes: 97 additions & 0 deletions geosongpu_ci/tools/py_ftn_interface/argument.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from typing import Optional
import yaml


class Argument:
def __init__(self, name: str, type: str, dims: Optional[int] = None) -> None:
self._name = name
self._type = type
self._dims = dims

@property
def name(self) -> str:
return self._name

@property
def name_sanitize(self) -> str:
if self._name in ["is", "in"]:
return f"_{self._name}"
return self.name

@property
def f90_dims_definition(self) -> str:
# (:,:)
return f"({(':,' * self._dims)[:-1]})" if self._dims else ""

@property
def f90_size_per_dims(self) -> str:
# size(var_name, 1), size(var_name, 2)
if not self._dims:
return ""
s = ""
for dim in range(0, self._dims):
s += f"size({self.name}, {dim+1}),"

return f"({s[:-1]})"

@property
def f90_dims_and_size(self) -> str:
# 2, size(var_name, 1), size(var_name, 2)
if not self._dims:
return ""
s = ""
for dim in range(0, self._dims):
s += f"size({self.name}, {dim+1}),"

return f"{self._dims},{s[:-1]}"

@property
def yaml_type(self) -> str:
return self._type

@property
def c_type(self) -> str:
if self._type.startswith("array_"):
return self._type[len("array_") :] + "*"
if self._type.startswith("MPI"):
return "void*"
return self._type

@property
def py_type_hint(self) -> str:
if self._type.startswith("array_"):
return "'cffi.FFI.CData'"
if self._type == "MPI":
return "MPI.Intercomm"
return self._type

@property
def f90_type_definition(self) -> str:
if self._type == "int":
return "integer(kind=c_int), value"
elif self._type == "float":
return "real(kind=c_float), value"
elif self._type == "double":
return "real(kind=c_double), value"
elif self._type == "array_int":
return "integer(kind=c_int), dimension(*)"
elif self._type == "array_float":
return "real(kind=c_float), dimension(*)"
elif self._type == "array_double":
return "real(kind=c_double), dimension(*)"
elif self._type == "MPI":
return "integer(kind=c_int), value"
else:
raise RuntimeError(f"ERROR_DEF_TYPE_TO_FORTRAN: {self._type}")


def argument_constructor(
loader: yaml.SafeLoader, node: yaml.nodes.MappingNode
) -> Argument:
return Argument(**loader.construct_mapping(node)) # noqa


def get_argument_yaml_loader():
loader = yaml.SafeLoader
loader.add_constructor("!Argument", argument_constructor)
return loader
133 changes: 133 additions & 0 deletions geosongpu_ci/tools/py_ftn_interface/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import jinja2
from typing import Any, Dict, List
import textwrap

from geosongpu_ci.tools.py_ftn_interface.argument import Argument


class Function:
def __init__(
self,
name: str,
inputs: List[Argument],
inouts: List[Argument],
outputs: List[Argument],
) -> None:
self.name = name
self._inputs = inputs
self._inouts = inouts
self._outputs = outputs

@property
def inputs(self) -> List[Argument]:
return self._inputs

@property
def outputs(self) -> List[Argument]:
return self._outputs

@property
def inouts(self) -> List[Argument]:
return self._inouts

@property
def arguments(self) -> List[Argument]:
return self._inputs + self._inouts + self._outputs

@staticmethod
def c_arguments_for_jinja2(arguments: List[Argument]) -> List[Dict[str, Any]]:
"""Transform yaml input for the template renderer"""
return [
{
"type": argument.c_type,
"name": argument.name,
"dims": argument._dims,
}
for argument in arguments
]

@staticmethod
def fortran_arguments_for_jinja2(arguments: List[Argument]) -> List[Dict[str, str]]:
"""Transform yaml input for the template renderer"""
return [
{
"name": argument.name,
"type": argument.f90_type_definition,
"dims_f90_defs": argument.f90_dims_definition,
"size_f90_per_dims": argument.f90_size_per_dims,
"f90_dims_and_size": argument.f90_dims_and_size,
}
for argument in arguments
]

@staticmethod
def py_arguments_for_jinja2(arguments: List[Argument]) -> List[Dict[str, str]]:
"""Transform yaml input for the template renderer"""
return [
{"type": argument.py_type_hint, "name": argument.name_sanitize}
for argument in arguments
]

def py_init_code(self) -> List[str]:
code = []
for argument in self.arguments:
if argument.yaml_type == "MPI":
code.append(
textwrap.dedent(
f"""\
# Comm translate to python
comm_py = MPI.Intracomm() # new comm, internal MPI_Comm handle is MPI_COMM_NULL
comm_ptr = MPI._addressof(comm_py) # internal MPI_Comm handle
comm_ptr = ffi.cast('{{_mpi_comm_t}}*', comm_ptr) # make it a CFFI pointer
comm_ptr[0] = {argument.name} # assign comm_c to comm_py's MPI_Comm handle
{argument.name} = comm_py # Override the symbol name to make life easier for code gen""" # noqa
)
)
return code

def c_init_code(self) -> List[str]:
prolog_code = []
for argument in self.arguments:
if argument.yaml_type == "MPI":
prolog_code.append(
f"MPI_Comm {argument.name}_c = MPI_Comm_f2c({argument.name});"
)
return prolog_code

def arguments_name(self) -> List[str]:
return [argument.name_sanitize for argument in self.arguments]

@staticmethod
def _fortran_type_declaration(def_type: str) -> str:
if def_type == "int":
return "integer(kind=c_int), value"
elif def_type == "float":
return "real(kind=c_float), value"
elif def_type == "double":
return "real(kind=c_double), value"
elif def_type == "array_int":
return "integer(kind=c_int), dimension(*)"
elif def_type == "array_float":
return "real(kind=c_float), dimension(*)"
elif def_type == "array_double":
return "real(kind=c_double), dimension(*)"
elif def_type == "MPI":
return "integer(kind=c_int), value"
else:
raise RuntimeError(f"ERROR_DEF_TYPE_TO_FORTRAN: {def_type}")


class InterfaceConfig:
def __init__(
self,
directory_path: str,
prefix: str,
function_defines: List[Function],
template_env: jinja2.Environment,
) -> None:
self._directory_path = directory_path
self._prefix = prefix
self._hook_obj = prefix
self._hook_class = prefix.upper()
self._functions = function_defines
self._template_env = template_env
Loading

0 comments on commit 6ab929f

Please sign in to comment.