Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python/Fortran interface generator: bases #27

Merged
merged 10 commits into from
Dec 15, 2023
Merged
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
Loading