From c16a9c2769a29bfc2284052f575c769bbbb6fff6 Mon Sep 17 00:00:00 2001
From: Kilian Weishaupt <kilian.weishaupt@iws.uni-stuttgart.de>
Date: Wed, 9 Jun 2021 19:25:17 +0200
Subject: [PATCH] [python] Add python properties and TypeTag

---
 python/dumux/common/CMakeLists.txt |   1 +
 python/dumux/common/properties.py  | 235 +++++++++++++++++++++++++++++
 2 files changed, 236 insertions(+)
 create mode 100644 python/dumux/common/properties.py

diff --git a/python/dumux/common/CMakeLists.txt b/python/dumux/common/CMakeLists.txt
index dc0edac4c6..ffc35a5823 100644
--- a/python/dumux/common/CMakeLists.txt
+++ b/python/dumux/common/CMakeLists.txt
@@ -1,5 +1,6 @@
 add_python_targets(common
   __init__
+  properties
 )
 dune_add_pybind11_module(NAME _common)
 set_property(TARGET _common PROPERTY LINK_LIBRARIES dunecommon dunegrid APPEND)
diff --git a/python/dumux/common/properties.py b/python/dumux/common/properties.py
new file mode 100644
index 0000000000..9b512be8fe
--- /dev/null
+++ b/python/dumux/common/properties.py
@@ -0,0 +1,235 @@
+import os
+
+"""
+Properties
+"""
+
+class Property:
+    """"Properties are used to construct a model"""
+
+    def __init__(self, object=None, value=None, type=None, includes=[], requiredProperties=[]):
+        if object is not None:
+            assert(hasattr(object, '_typeName'))
+            if type is not None or value is not None:
+                raise ValueError("The Property constructor expects exactly one of the following arguments: object, type, or value.")
+            if includes or requiredProperties:
+                raise ValueError("The arguments includes and requiredProperties are ignored if the object argument is specified.")
+            self._typeName = object._typeName
+            self._includes = object._includes if hasattr(object, '_includes') else []
+            self._requiredPropertyTypes = object._requiredPropertyTypes if hasattr(object, '_requiredPropertyTypes') else []
+        elif value is not None:
+            if object is not None or type is not None:
+                raise ValueError("The Property constructor expects exactly one of the following arguments: object, type, or value.")
+            if includes or requiredProperties:
+                raise ValueError("The arguments includes and requiredProperties are ignored if the value argument is specified.")
+            self._value = value
+        elif type is not None:
+            if object is not None or value is not None:
+                raise ValueError("The Property constructor expects exactly one of the following arguments: object, type, or value.")
+            self._typeName = type
+            self._includes = includes
+            self._requiredPropertyTypes = requiredProperties
+        else:
+            raise ValueError("The Property constructor expects exactly one of the following arguments: object, type, or value.")
+
+
+def typePropertyToString(propertyName, typeTagName, typeArg):
+    """Converts a Type Property to a string"""
+
+    propertyString = 'template<class TypeTag>\n'
+    propertyString += 'struct {}<TypeTag, TTag::{}>\n{{\n'.format(propertyName, typeTagName)
+
+    if isinstance(typeArg, (float)):
+        propertyString += '    using type = {};\n'.format('double')
+    elif isinstance(typeArg, (int)):
+        propertyString += '    using type = {};\n'.format('int')
+    elif isinstance(typeArg, Property) or not isinstance(typeArg, (str)):
+        if hasattr(typeArg, '_requiredPropertyTypes') or hasattr(typeArg, '_requiredPropertyValues'):
+            propertyString += 'private:\n'
+        if hasattr(typeArg, '_requiredPropertyTypes'):
+            for reqProp in typeArg._requiredPropertyTypes:
+                propertyString += '    using {} = {};\n'.format(reqProp, 'GetPropType<TypeTag, Properties::{}>'.format(reqProp))
+
+        if hasattr(typeArg, '_requiredPropertyValues'):
+            for reqProp in typeArg._requiredPropertyValues:
+                reqPropLowerCase = reqProp[0].lower() + reqProp[1:]
+                propertyString += '    static constexpr auto {} = {};\n'.format(reqPropLowerCase, 'getPropValue<TypeTag, Properties::{}>()'.format(reqProp))
+
+        propertyString += 'public:\n'
+        propertyString += '    using type = {};\n'.format(typeArg._typeName)
+
+    propertyString += '};'
+
+    return propertyString
+
+
+def valuePropertyToString(propertyName, typeTagName, value):
+    """Converts a Value Property to a string"""
+
+    propertyString = 'template<class TypeTag>\n'
+    propertyString += 'struct {}<TypeTag, TTag::{}>\n{{'.format(propertyName, typeTagName)
+
+    # make sure to get the correct C++ types and values
+    if isinstance(value, bool):
+        value = str(value).lower()
+        type = 'bool'
+    elif isinstance(value, int):
+        type = 'int'
+    else:
+        type = 'Scalar'
+        propertyString += '\nprivate:\n'
+        propertyString += '    using Scalar = GetPropType<TypeTag, Properties::Scalar>;\n'
+        propertyString += 'public:'
+
+    propertyString += '\n    static constexpr {} value = {};\n'.format(type, value)
+    propertyString += '};'
+
+    return propertyString
+
+existingTypeTags = {'CCTpfaModel':{'include':'dumux/discretization/cctpfa.hh',
+                                   'description':'A cell-centered two-point flux finite volume discretization scheme.'},
+                    'BoxModel':{'include':'dumux/discretization/box.hh',
+                                'description':'A node-centered two-point flux finite volume discretization scheme'},
+                    'OneP':{'include':'dumux/porousmediumflow/1p/model.hh',
+                            'description':'A model for single-phase flow in porous media.'}}
+
+
+def listTypeTags():
+    """List all available TypeTags/Models that can be inherited from"""
+
+    print("\n**********************************\n")
+    print("The following TypeTags are availabe:")
+    for key in existingTypeTags.keys():
+        print(key,":", existingTypeTags[key]['description'])
+    print("\n**********************************")
+
+
+def getKnownProperties():
+    filepath = os.path.abspath(os.path.dirname(__file__) + '/../../../../dumux/common/properties.hh')
+    with open(filepath) as f:
+        result = []
+        for line in f:
+            if line.startswith('struct'):
+                result.append(line.split(' ')[1])
+        return result
+
+
+class TypeTag:
+    knownProperties = getKnownProperties()
+
+    def __init__(self, name, *, inheritsFrom=None, gridGeometry=None, scalar='double'):
+        self.name = name
+        self.inheritsFrom = inheritsFrom
+        self.includes = []
+        self.properties = {}
+        self.newPropertyDefinitions = []
+        self.gridGeometry = gridGeometry
+
+        if name in existingTypeTags.keys():
+            if inheritsFrom is not None:
+                raise ValueError("Existing TypeTag {} cannot inherit from other TypeTags. Use TypeTag({}) only.".format(name, name))
+            self.isExistingTypeTag = True
+            self.includes = [existingTypeTags[name]['include']]
+        else:
+            self.isExistingTypeTag = False
+
+        if self.inheritsFrom is not None:
+            # treat existing TypeTags by converting the given string to a real TypeTag object
+            for idx, parentTypeTag in enumerate(self.inheritsFrom):
+                if not isinstance(parentTypeTag, TypeTag):
+                    if not isinstance(parentTypeTag, str):
+                        raise ValueError("Unknown parent TypeTag {}. Use either argument of type TypeTag "
+                                         "or a string for an existing TypeTag. List of existing TypeTags: {}".format(parentTypeTag, existingTypeTags.keys()))
+                    if parentTypeTag not in existingTypeTags.keys():
+                        raise ValueError("Unknown TypeTag {}. List of existing TypeTags: {}".format(parentTypeTag, existingTypeTags.keys()))
+                    self.inheritsFrom[idx] = TypeTag(parentTypeTag)
+
+            # pick up the properties and includes of the parent TypeTag
+            for parentTypeTag in reversed(self.inheritsFrom):
+                self.newPropertyDefinitions += parentTypeTag.newPropertyDefinitions
+                for key in parentTypeTag.properties:
+                    self.properties[key] = parentTypeTag.properties[key]
+                if parentTypeTag.includes is not None:
+                    for include in parentTypeTag.includes:
+                        self.includes.append(include)
+
+        self._typeName = 'Dumux::Properties::TTag::' + name
+
+        # set the scalar type
+        self.__setitem__('Scalar', Property(type=scalar))
+
+    # the [] operator for setting values
+    def __setitem__(self, key, value):
+        if not isinstance(value, Property):
+            raise ValueError('Only values of type Property can be assigned to a model')
+
+        if key not in self.knownProperties:
+            print(f'Adding {key} as new property')
+            self.newPropertyDefinitions += [key]
+
+        self.properties[key] = value
+        if hasattr(value, '_includes'):
+            for include in value._includes:
+                self.includes.append(include)
+
+    # the [] operator for getting values
+    def __getitem__(self, key):
+        return self.properties[key]
+
+    # returns the TypeTag as a string
+    def getTypeTag(self):
+        return 'Dumux::Properties::TTag::' + self.name
+
+    # creates a string resembling a properties.hh file
+    def getProperties(self):
+        file = '#ifndef DUMUX_{}_PROPERTIES_HH\n'.format(self.name.upper())
+        file += '#define DUMUX_{}_PROPERTIES_HH\n\n'.format(self.name.upper())
+
+        file += '#include <dumux/common/properties.hh>\n'
+
+        for include in self.includes:
+            assert('<' not in include)
+            file += '#include <{}>\n'.format(include)
+
+        file += '\n'
+
+        file += 'namespace Dumux::Properties {\n\n'
+        file += 'namespace TTag {\n'
+
+        if self.inheritsFrom is not None:
+            for otherTypeTag in self.inheritsFrom:
+                if not otherTypeTag.isExistingTypeTag:
+                    file += 'struct {}{{}}; \n'.format(otherTypeTag.name)
+
+            args = ",".join(x.name for x in self.inheritsFrom)
+        else:
+            args = ""
+
+        file += f'struct {self.name}\n{{\n'
+        file += f'    using InheritsFrom = std::tuple<{args}>;\n'
+        if self.gridGeometry is not None:
+           file += f'    using GridGeometry = {self.gridGeometry._typeName};\n'
+        file += '};\n} // end namespace TTag\n\n'
+
+        for newDef in self.newPropertyDefinitions:
+            file += 'template<class TypeTag, class MyTypeTag>\n'
+            file += 'struct ' + newDef + ' { using type =  UndefinedProperty; };\n\n'
+
+        for prop in self.properties:
+            if hasattr(self[prop], '_value'):
+                file += valuePropertyToString(prop, self.name, self[prop]._value) +'\n\n'
+            else:
+                file += typePropertyToString(prop, self.name, self[prop]) +'\n\n'
+
+        if self.gridGeometry is not None:
+            file += typePropertyToString('Grid', self.name, Property(type='typename TypeTag::GridGeometry::Grid')) +'\n\n'
+
+        file += '} // end namespace Dumux::Properties \n\n'
+        file += '#endif'
+
+        return file
+
+
+class Model(TypeTag):
+    # TODO maybe rename TypeTag to Model and remove this class here
+    pass
-- 
GitLab