Repository: NTNU-IHB/PythonFMU (python)

Issue:
Provide start values for variables that requires it
According to the spec, some variables with a given causality/variability must provide a start value. This is currently not the case. At least not automatically. 

Pull request description:
Fix #16
@fcollonval Care to have a look? I did not figure out how to set the instances `__attrd` from outside the class, so I made a setter. Also it should be tested, but I dunno how to access the start xml element using pyfmi.

Closes #16 

Patch:
diff --git a/README.md b/README.md
index c918ee5..a900c61 100644
--- a/README.md
+++ b/README.md
@@ -51,7 +51,6 @@ slave_class = "PythonSlave"  # REQUIRED - Name of the class extending Fmi2Slave
 class PythonSlave(Fmi2Slave):
 
     author = "John Doe"
-    modelName = "PythonSlave"  # REQUIRED
     description = "A simple description"
 
     def __init__(self):
diff --git a/pythonfmu/fmi2slave.py b/pythonfmu/fmi2slave.py
index f9d9408..738713f 100644
--- a/pythonfmu/fmi2slave.py
+++ b/pythonfmu/fmi2slave.py
@@ -6,7 +6,7 @@ from typing import ClassVar, Dict, List, Optional
 from uuid import uuid1
 from xml.etree.ElementTree import Element, SubElement
 
-from .enums import Fmi2Causality
+from .enums import Fmi2Causality, Fmi2Initial, Fmi2Variability
 from .variables import Boolean, Integer, Real, ScalarVariable, String
 
 ModelOptions = namedtuple('ModelOptions', ['name', 'value', 'cli'])
@@ -35,7 +35,7 @@ class Fmi2Slave(ABC):
     def __init__(self):
         self.vars = dict()
         if self.modelName is None:
-            raise Exception("No modelName has been specified!")
+            self.modelName = self.__class__.__name__
 
     def to_xml(self, model_options: Dict[str, str] = dict()) -> Element:
         """Build the XML representation of the model.
@@ -82,6 +82,8 @@ class Fmi2Slave(ABC):
 
         variables = SubElement(root, 'ModelVariables')
         for v in self.vars.values():
+            if ScalarVariable.requires_start(v):
+                self.__apply_start_value(v)
             variables.append(v.to_xml())
 
         structure = SubElement(root, 'ModelStructure')
@@ -94,6 +96,23 @@ class Fmi2Slave(ABC):
 
         return root
 
+    def __apply_start_value(self, var: ScalarVariable):
+        refs = [None]
+        vrs = [var.value_reference]
+
+        if isinstance(var, Integer):
+            self.__get_integer__(vrs, refs)
+        elif isinstance(var, Real):
+            self.__get_real__(vrs, refs)
+        elif isinstance(var, Boolean):
+            self.__get_boolean__(vrs, refs)
+        elif isinstance(var, String):
+            self.__get_string__(vrs, refs)
+        else:
+            raise Exception(f"Unsupported type!")
+
+        var.start = refs[0]
+
     def register_variable(self, var: ScalarVariable):
         variable_reference = len(self.vars)
         self.vars[variable_reference] = var
diff --git a/pythonfmu/variables.py b/pythonfmu/variables.py
index 249a4d1..217903f 100644
--- a/pythonfmu/variables.py
+++ b/pythonfmu/variables.py
@@ -55,6 +55,14 @@ class ScalarVariable(ABC):
     def variability(self) -> Optional[Fmi2Variability]:
         return self.__attrs["variability"]
 
+    @staticmethod
+    def requires_start(v) -> bool:
+        return v.initial == Fmi2Initial.exact or \
+               v.initial == Fmi2Initial.approx or \
+               v.causality == Fmi2Causality.input or \
+               v.causality == Fmi2Causality.parameter or \
+               v.variability == Fmi2Variability.constant
+
     def to_xml(self) -> Element:
         attrib = dict()
         for key, value in self.__attrs.items():
@@ -72,6 +80,10 @@ class Real(ScalarVariable):
     def start(self) -> Optional[Any]:
         return self.__attrs["start"]
 
+    @start.setter
+    def start(self, value: float):
+        self.__attrs["start"] = value
+
     def to_xml(self) -> Element:
         attrib = dict()
         for key, value in self.__attrs.items():
@@ -92,6 +104,10 @@ class Integer(ScalarVariable):
     def start(self) -> Optional[Any]:
         return self.__attrs["start"]
 
+    @start.setter
+    def start(self, value: float):
+        self.__attrs["start"] = value
+
     def to_xml(self) -> Element:
         attrib = dict()
         for key, value in self.__attrs.items():
@@ -112,6 +128,10 @@ class Boolean(ScalarVariable):
     def start(self) -> Optional[Any]:
         return self.__attrs["start"]
 
+    @start.setter
+    def start(self, value: float):
+        self.__attrs["start"] = value
+
     def to_xml(self) -> Element:
         attrib = dict()
         for key, value in self.__attrs.items():
@@ -132,6 +152,10 @@ class String(ScalarVariable):
     def start(self) -> Optional[Any]:
         return self.__attrs["start"]
 
+    @start.setter
+    def start(self, value: float):
+        self.__attrs["start"] = value
+
     def to_xml(self) -> Element:
         attrib = dict()
         for key, value in self.__attrs.items():


Test patch:
diff --git a/pythonfmu/tests/pythonslave.py b/pythonfmu/tests/pythonslave.py
index 3fe0a7c..034c108 100644
--- a/pythonfmu/tests/pythonslave.py
+++ b/pythonfmu/tests/pythonslave.py
@@ -1,4 +1,4 @@
-from pythonfmu.fmi2slave import Fmi2Slave, Fmi2Causality, Integer, Real, Boolean, String
+from pythonfmu.fmi2slave import Fmi2Slave, Fmi2Causality, Fmi2Variability, Integer, Real, Boolean, String
 
 slave_class = "PythonSlave"  # REQUIRED - Name of the class extending Fmi2Slave
 
@@ -6,17 +6,17 @@ slave_class = "PythonSlave"  # REQUIRED - Name of the class extending Fmi2Slave
 class PythonSlave(Fmi2Slave):
 
     author = "John Doe"
-    modelName = "PythonSlave"  # REQUIRED
     description = "A simple description"
 
     def __init__(self):
         super().__init__()
 
-        self.intOut = 1
+        self.intParam = 42
         self.realOut = 3.0
         self.booleanVariable = True
         self.stringVariable = "Hello World!"
-        self.register_variable(Integer("intOut", causality=Fmi2Causality.output))
+        self.register_variable(
+            Integer("intParam", causality=Fmi2Causality.parameter, variability=Fmi2Variability.tunable))
         self.register_variable(Real("realOut", causality=Fmi2Causality.output))
         self.register_variable(Boolean("booleanVariable", causality=Fmi2Causality.local))
         self.register_variable(String("stringVariable", causality=Fmi2Causality.local))
diff --git a/pythonfmu/tests/test_integration.py b/pythonfmu/tests/test_integration.py
index 7212223..78765e7 100644
--- a/pythonfmu/tests/test_integration.py
+++ b/pythonfmu/tests/test_integration.py
@@ -58,14 +58,12 @@ slave_class = "PythonSlaveWithDep"
 
 class PythonSlaveWithDep(Fmi2Slave):
 
-    modelName = "PythonSlaveWithDep"
-
     def __init__(self):
         super().__init__()
 
         self.realIn = 22.0
         self.realOut = 0.0
-        self.register_variable(Real("realIn", start=22., causality=Fmi2Causality.input))
+        self.register_variable(Real("realIn", causality=Fmi2Causality.input))
         self.register_variable(Real("realOut", causality=Fmi2Causality.output))
 
     def do_step(self, current_time, step_size):
diff --git a/pythonfmu/tests/test_variables.py b/pythonfmu/tests/test_variables.py
index 57d1a39..2597348 100644
--- a/pythonfmu/tests/test_variables.py
+++ b/pythonfmu/tests/test_variables.py
@@ -3,6 +3,7 @@ from random import randint
 
 import pytest
 
+from pythonfmu import Fmi2Slave
 from pythonfmu.enums import Fmi2Causality, Fmi2Initial, Fmi2Variability
 from pythonfmu.variables import Boolean, Integer, Real, ScalarVariable, String
 
@@ -58,6 +59,42 @@ def test_ScalarVariable_to_xml(causality, initial, variability, name, descriptio
             else:
                 assert node.attrib[attr] == str(value)
 
+
+@pytest.mark.parametrize("var_type, value", [
+    (Boolean, True),
+    (Integer, 23),
+    (Real, 15.),
+    (String, "hello")])
+@pytest.mark.parametrize("causality", list(Fmi2Causality) + [None, ])
+@pytest.mark.parametrize("initial", list(Fmi2Initial) + [None, ])
+@pytest.mark.parametrize("variability", list(Fmi2Variability) + [None, ])
+def test_ScalarVariable_start(var_type, value, causality, initial, variability):
+    var_obj = var_type("var", causality=causality, description="a variable", initial=initial, variability=variability)
+
+    class PySlave(Fmi2Slave):
+
+        modelName = "PySlave"
+
+        def __init__(self):
+            super().__init__()
+            setattr(self, "var", value)
+            self.register_variable(var_obj)
+
+        def do_step(self, current_time: float, step_size: float):
+            return True
+
+    slave = PySlave()
+
+    xml = slave.to_xml()
+    var_node = xml.find(".//ScalarVariable[@name='var']")
+    assert var_node is not None
+
+    if ScalarVariable.requires_start(var_obj):
+        assert var_obj.start == value
+    else:
+        assert var_obj.start is None
+
+
 @pytest.mark.parametrize("name,start", [
     ("boolean_name", None),
     ("boolean_another_name", False),