Claudio Gomes vor 6 Jahren
Ursprung
Commit
eefcb19962
65 geänderte Dateien mit 4594 neuen und 3 gelöschten Zeilen
  1. 9 0
      .gitignore
  2. 67 3
      README.md
  3. 27 0
      build.sbt
  4. BIN
      lib/fmi2-sources.zip
  5. BIN
      lib/fmi2.jar
  6. BIN
      lib/jnifmuapi-sources.jar
  7. BIN
      lib/jnifmuapi.jar
  8. BIN
      lib/modbat.jar
  9. 5 0
      lib/nativebinaries/lib/Linux-amd64/git-info.txt
  10. BIN
      lib/nativebinaries/lib/Linux-amd64/libfmuapi.so
  11. 5 0
      lib/nativebinaries/lib/Linux-i386/git-info.txt
  12. BIN
      lib/nativebinaries/lib/Linux-i386/libfmuapi.so
  13. 5 0
      lib/nativebinaries/lib/Mac-x86_64/git-info.txt
  14. BIN
      lib/nativebinaries/lib/Mac-x86_64/libfmuapi.dylib
  15. BIN
      lib/nativebinaries/lib/Windows-amd64/fmuapi.dll
  16. 5 0
      lib/nativebinaries/lib/Windows-amd64/git-info.txt
  17. BIN
      lib/nativebinaries/lib/Windows-x86/fmuapi.dll
  18. 5 0
      lib/nativebinaries/lib/Windows-x86/git-info.txt
  19. 1 0
      project/build.properties
  20. 2 0
      project/plugins.sbt
  21. 1354 0
      src/main/java/org/intocps/orchestration/coe/modeldefinition/ModelDescription.java
  22. 76 0
      src/main/java/org/intocps/orchestration/coe/modeldefinition/xml/NamedNodeMapIterator.java
  23. 76 0
      src/main/java/org/intocps/orchestration/coe/modeldefinition/xml/NodeIterator.java
  24. 227 0
      src/main/resources/graphs/fmi/fmi.graphml
  25. BIN
      src/main/resources/graphs/fmi/fmi.png
  26. 55 0
      src/main/resources/graphs/fmi/free.graphml
  27. BIN
      src/main/resources/graphs/fmi/free.png
  28. 68 0
      src/main/resources/graphs/fmi/reset.graphml
  29. 3 0
      src/main/resources/simplelogger.properties
  30. 12 0
      src/main/scala/Config.scala
  31. 214 0
      src/main/scala/FMIGraphModel.scala
  32. 99 0
      src/main/scala/FMIModel.scala
  33. 105 0
      src/main/scala/FMUAnalyzerApp.scala
  34. 32 0
      src/main/scala/FMUDynamicData.scala
  35. 130 0
      src/main/scala/FMUHandler.scala
  36. 54 0
      src/main/scala/FMUStaticData.scala
  37. 5 0
      src/main/scala/FmuInvocationStatusException.scala
  38. 239 0
      src/main/scala/GraphMLLoader.scala
  39. 11 0
      src/main/scala/GraphMLTypes.scala
  40. 114 0
      src/main/scala/InputApproximationAnalyzer.scala
  41. 155 0
      src/main/scala/MBTRunner.scala
  42. 95 0
      src/main/scala/ModBatGraphModel.scala
  43. 8 0
      src/main/scala/ModBatResults.scala
  44. 15 0
      src/main/scala/ScalarFMUModel.scala
  45. 96 0
      src/main/scala/SensitivityAnalyzer.scala
  46. BIN
      src/test/resources/fmus/20-sim/threewatertank1.fmu
  47. BIN
      src/test/resources/fmus/20-sim/threewatertank2.fmu
  48. BIN
      src/test/resources/fmus/20-sim/threewatertankcontroller2.fmu
  49. BIN
      src/test/resources/fmus/dymola/MassSpringDamper1.fmu
  50. BIN
      src/test/resources/fmus/dymola/MassSpringDamper2.fmu
  51. BIN
      src/test/resources/fmus/omodelica/MassSpringDamper2.fmu
  52. BIN
      src/test/resources/fmus/omodelica/Scalar.fmu
  53. 95 0
      src/test/resources/graphs/bound.graphml
  54. 95 0
      src/test/resources/graphs/flat.graphml
  55. 149 0
      src/test/resources/graphs/hierarchical.graphml
  56. 67 0
      src/test/resources/graphs/splitted_graphs/1.graphml
  57. 54 0
      src/test/resources/graphs/splitted_graphs/2.graphml
  58. 66 0
      src/test/resources/graphs/splitted_graphs/3.graphml
  59. 54 0
      src/test/resources/graphs/splitted_graphs/4.graphml
  60. 54 0
      src/test/resources/graphs/sugar_all.graphml
  61. 95 0
      src/test/resources/graphs/sugar_edges.graphml
  62. 54 0
      src/test/resources/graphs/sugar_nodes.graphml
  63. 10 0
      src/test/resources/models/Scalar.mo
  64. 241 0
      src/test/scala/GraphMLTests.scala
  65. 186 0
      src/test/scala/MiscTests.scala

+ 9 - 0
.gitignore

@@ -0,0 +1,9 @@
+/project/target/
+/project/project/target/
+/target/
+.idea
+/release/fmuanalyzer/*.log
+/release/fmuanalyzer/results/
+/release/fmuanalyzer/ClassEAmplifier_.results
+/release/fmuanalyzer/*.jar
+

+ 67 - 3
README.md

@@ -1,3 +1,67 @@
-# FMIMOBSTER
-
-FMIMOBSTER (Functional Mockup Interface Model Based Tester) is a tool that applies the principles of model based testing to improve the conformance of Functional Mockup Units to the Functional Mockup Interface Standard.
+# FMIMOBSTER
+
+FMIMOBSTER (Functional Mockup Interface Model Based Tester) is a tool that applies the principles of model based testing to improve the conformance of Functional Mockup Units to the Functional Mockup Interface Standard.
+
+This is a command line application that tests an FMU.
+It uses the a slightly modified version of the tool [[modbat](https://github.com/cyrille-artho/modbat)] to generate random walks in a graph that represents all possible interactions with an FMU.
+
+Such graph looks like the following.
+
+![fmi](src/main/resources/graphs/fmi/fmi.png)
+
+Each edge refers to a method that is implemented in a Scala class.
+
+The graph is enriched with state machines from other files. For example:
+
+![free](src/main/resources/graphs/fmi/free.png)
+
+You can see the files used to created the state machine in [[graphs](./src/main/resources/graphs)].
+
+If, during a test, the unexpected happens, the tool produces a error trace and a seed, that allows for the reproducibility of the test.
+
+## Installing and Running the App
+
+Download the app jar.
+
+Run the following, for the list of commands:
+
+```
+java -jar FMUAnalyzer-assembly-X.Y.jar --help
+```
+
+where `X.Y` denotes the app's version.
+
+## Experiment Workflow
+
+To run the model based testing on an FMU in path `path/to/fmu.fmu`, the following command is used:
+
+```
+java -D"org.slf4j.simpleLogger.defaultLogLevel=trace"  -jar .\FMUAnalyzer-assembly-X.Y.jar mbt -d path/to/fmu.fmu -n 10 -l 10 -o "results"
+```
+
+where 
+
+```
+-n is the number of random walks to produce
+-l is the maximum number of self-loops executed
+-o is the results folder
+X.Y denotes the app's version
+```
+
+While the app supports running a command in a directory of FMUs, it is advised to run the app for each FMU individually, *and in a separate process*. This is because some FMUs might crash the java process, which means the other FMUs did not get tested.
+
+Unfortunately, the app does not spawn separate processes for each walk.
+
+## Setup Dev Env
+
+This is a simple sbt project.
+
+See  [build.properties](project\build.properties)  and  [build.sbt](build.sbt)  for sbt and scala versions.
+
+The following will produce the jar of the app.
+
+```
+sbt assembly
+```
+
+## 

+ 27 - 0
build.sbt

@@ -0,0 +1,27 @@
+name := "FMUAnalyzer"
+//maintainer := "claudio.gomes@uantwerp.be"
+version := "0.1"
+
+scalaVersion := "2.11.12"
+
+libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.0-SNAP10"
+libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.14.0"
+
+libraryDependencies += "org.slf4j" % "slf4j-api" % "1.7.25"
+libraryDependencies += "org.slf4j" % "slf4j-simple" % "1.7.25"
+
+libraryDependencies += "org.apache.commons" % "commons-compress" % "1.5"
+
+libraryDependencies += "commons-io" % "commons-io" % "2.6"
+
+libraryDependencies += "commons-beanutils" % "commons-beanutils" % "1.9.3"
+
+libraryDependencies += "org.apache.commons" % "commons-lang3" % "3.9"
+
+libraryDependencies += "org.scalanlp" %% "breeze" % "1.0-RC4"
+
+libraryDependencies += "org.scalanlp" %% "breeze-viz" % "1.0-RC4"
+
+libraryDependencies += "com.github.scopt" %% "scopt" % "4.0.0-RC2"
+
+libraryDependencies += "org.scala-graph" %% "graph-core" % "1.12.5"

BIN
lib/fmi2-sources.zip


BIN
lib/fmi2.jar


BIN
lib/jnifmuapi-sources.jar


BIN
lib/jnifmuapi.jar


BIN
lib/modbat.jar


+ 5 - 0
lib/nativebinaries/lib/Linux-amd64/git-info.txt

@@ -0,0 +1,5 @@
+Branch: 
+* (HEAD detached at 84522d9) 84522d9 Fixes #3 - frees allocated memory
+  remotes/origin/development 84522d9 Fixes #3 - frees allocated memory
+SHA1: 
+84522d9103a3899e0f0e4d9014fd0b0cddddbda8

BIN
lib/nativebinaries/lib/Linux-amd64/libfmuapi.so


+ 5 - 0
lib/nativebinaries/lib/Linux-i386/git-info.txt

@@ -0,0 +1,5 @@
+Branch: 
+* (HEAD detached at 84522d9) 84522d9 Fixes #3 - frees allocated memory
+  remotes/origin/development 84522d9 Fixes #3 - frees allocated memory
+SHA1: 
+84522d9103a3899e0f0e4d9014fd0b0cddddbda8

BIN
lib/nativebinaries/lib/Linux-i386/libfmuapi.so


+ 5 - 0
lib/nativebinaries/lib/Mac-x86_64/git-info.txt

@@ -0,0 +1,5 @@
+Branch: 
+* (HEAD detached at 84522d9) 84522d9 Fixes #3 - frees allocated memory
+  remotes/origin/development 84522d9 Fixes #3 - frees allocated memory
+SHA1: 
+84522d9103a3899e0f0e4d9014fd0b0cddddbda8

BIN
lib/nativebinaries/lib/Mac-x86_64/libfmuapi.dylib


BIN
lib/nativebinaries/lib/Windows-amd64/fmuapi.dll


+ 5 - 0
lib/nativebinaries/lib/Windows-amd64/git-info.txt

@@ -0,0 +1,5 @@
+Branch: 
+* (HEAD detached at 84522d9) 84522d9 Fixes #3 - frees allocated memory
+  remotes/origin/development 84522d9 Fixes #3 - frees allocated memory
+SHA1: 
+84522d9103a3899e0f0e4d9014fd0b0cddddbda8

BIN
lib/nativebinaries/lib/Windows-x86/fmuapi.dll


+ 5 - 0
lib/nativebinaries/lib/Windows-x86/git-info.txt

@@ -0,0 +1,5 @@
+Branch: 
+* (HEAD detached at 84522d9) 84522d9 Fixes #3 - frees allocated memory
+  remotes/origin/development 84522d9 Fixes #3 - frees allocated memory
+SHA1: 
+84522d9103a3899e0f0e4d9014fd0b0cddddbda8

+ 1 - 0
project/build.properties

@@ -0,0 +1 @@
+sbt.version = 1.2.8

+ 2 - 0
project/plugins.sbt

@@ -0,0 +1,2 @@
+// See https://github.com/sbt/sbt-assembly
+addSbtPlugin("com.eed3si9n" % "sbt-assembly" % "0.14.10")

Datei-Diff unterdrückt, da er zu groß ist
+ 1354 - 0
src/main/java/org/intocps/orchestration/coe/modeldefinition/ModelDescription.java


+ 76 - 0
src/main/java/org/intocps/orchestration/coe/modeldefinition/xml/NamedNodeMapIterator.java

@@ -0,0 +1,76 @@
+/*
+* This file is part of the INTO-CPS toolchain.
+*
+* Copyright (c) 2017-CurrentYear, INTO-CPS Association,
+* c/o Professor Peter Gorm Larsen, Department of Engineering
+* Finlandsgade 22, 8200 Aarhus N.
+*
+* All rights reserved.
+*
+* THIS PROGRAM IS PROVIDED UNDER THE TERMS OF GPL VERSION 3 LICENSE OR
+* THIS INTO-CPS ASSOCIATION PUBLIC LICENSE VERSION 1.0.
+* ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES
+* RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GPL 
+* VERSION 3, ACCORDING TO RECIPIENTS CHOICE.
+*
+* The INTO-CPS toolchain  and the INTO-CPS Association Public License 
+* are obtained from the INTO-CPS Association, either from the above address,
+* from the URLs: http://www.into-cps.org, and in the INTO-CPS toolchain distribution.
+* GNU version 3 is obtained from: http://www.gnu.org/copyleft/gpl.html.
+*
+* This program is distributed WITHOUT ANY WARRANTY; without
+* even the implied warranty of  MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH IN THE
+* BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF
+* THE INTO-CPS ASSOCIATION.
+*
+* See the full INTO-CPS Association Public License conditions for more details.
+*/
+
+/*
+* Author:
+*		Kenneth Lausdahl
+*		Casper Thule
+*/
+package org.intocps.orchestration.coe.modeldefinition.xml;
+
+import java.util.Iterator;
+
+import org.w3c.dom.NamedNodeMap;
+import org.w3c.dom.Node;
+
+public class NamedNodeMapIterator implements Iterator<Node>, Iterable<Node>
+{
+	private final NamedNodeMap list;
+	private int index = 0;
+
+	public NamedNodeMapIterator(NamedNodeMap list)
+	{
+		this.list = list;
+	}
+
+	@Override
+	public boolean hasNext()
+	{
+		return list != null && index < list.getLength();
+	}
+
+	@Override
+	public Node next()
+	{
+		return list.item(index++);
+	}
+
+	@Override
+	public void remove()
+	{
+		throw new RuntimeException("Not implemented");
+	}
+
+	@Override
+	public Iterator<Node> iterator()
+	{
+		return this;
+	}
+
+}

+ 76 - 0
src/main/java/org/intocps/orchestration/coe/modeldefinition/xml/NodeIterator.java

@@ -0,0 +1,76 @@
+/*
+* This file is part of the INTO-CPS toolchain.
+*
+* Copyright (c) 2017-CurrentYear, INTO-CPS Association,
+* c/o Professor Peter Gorm Larsen, Department of Engineering
+* Finlandsgade 22, 8200 Aarhus N.
+*
+* All rights reserved.
+*
+* THIS PROGRAM IS PROVIDED UNDER THE TERMS OF GPL VERSION 3 LICENSE OR
+* THIS INTO-CPS ASSOCIATION PUBLIC LICENSE VERSION 1.0.
+* ANY USE, REPRODUCTION OR DISTRIBUTION OF THIS PROGRAM CONSTITUTES
+* RECIPIENT'S ACCEPTANCE OF THE OSMC PUBLIC LICENSE OR THE GPL 
+* VERSION 3, ACCORDING TO RECIPIENTS CHOICE.
+*
+* The INTO-CPS toolchain  and the INTO-CPS Association Public License 
+* are obtained from the INTO-CPS Association, either from the above address,
+* from the URLs: http://www.into-cps.org, and in the INTO-CPS toolchain distribution.
+* GNU version 3 is obtained from: http://www.gnu.org/copyleft/gpl.html.
+*
+* This program is distributed WITHOUT ANY WARRANTY; without
+* even the implied warranty of  MERCHANTABILITY or FITNESS FOR
+* A PARTICULAR PURPOSE, EXCEPT AS EXPRESSLY SET FORTH IN THE
+* BY RECIPIENT SELECTED SUBSIDIARY LICENSE CONDITIONS OF
+* THE INTO-CPS ASSOCIATION.
+*
+* See the full INTO-CPS Association Public License conditions for more details.
+*/
+
+/*
+* Author:
+*		Kenneth Lausdahl
+*		Casper Thule
+*/
+package org.intocps.orchestration.coe.modeldefinition.xml;
+
+import java.util.Iterator;
+
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+public class NodeIterator implements Iterator<Node>, Iterable<Node>
+{
+	private final NodeList list;
+	private int index = 0;
+
+	public NodeIterator(NodeList list)
+	{
+		this.list = list;
+	}
+
+	@Override
+	public boolean hasNext()
+	{
+		return index < list.getLength();
+	}
+
+	@Override
+	public Node next()
+	{
+		return list.item(index++);
+	}
+
+	@Override
+	public void remove()
+	{
+		throw new RuntimeException("Not implemented");
+	}
+
+	@Override
+	public Iterator<Node> iterator()
+	{
+		return this;
+	}
+
+}

Datei-Diff unterdrückt, da er zu groß ist
+ 227 - 0
src/main/resources/graphs/fmi/fmi.graphml


BIN
src/main/resources/graphs/fmi/fmi.png


Datei-Diff unterdrückt, da er zu groß ist
+ 55 - 0
src/main/resources/graphs/fmi/free.graphml


BIN
src/main/resources/graphs/fmi/free.png


Datei-Diff unterdrückt, da er zu groß ist
+ 68 - 0
src/main/resources/graphs/fmi/reset.graphml


+ 3 - 0
src/main/resources/simplelogger.properties

@@ -0,0 +1,3 @@
+org.slf4j.simpleLogger.showDateTime=true
+org.slf4j.simpleLogger.defaultLogLevel=info
+org.slf4j.simpleLogger.dateTimeFormat=yyyy-MM-dd HH:mm:ss:SSS

+ 12 - 0
src/main/scala/Config.scala

@@ -0,0 +1,12 @@
+import java.io.File
+import java.math.BigInteger
+case class Config(
+                   dir: File = new File("."),
+                   model: String = "",
+                   nruns: Int = 1,
+                   loopLimit: Int = 10,
+                   seed: BigInteger = null,
+                   stopOnFailure: Boolean = false,
+                   deleteOnSuccess: Boolean = false,
+                   mode: String = "",
+                   outdir: String = "./results")

+ 214 - 0
src/main/scala/FMIGraphModel.scala

@@ -0,0 +1,214 @@
+import java.lang
+
+import modbat.RequirementFailedException
+import org.intocps.fmi.{Fmi2Status, Fmi2StatusKind, IFmiComponent}
+import org.intocps.orchestration.coe.modeldefinition.ModelDescription
+import org.intocps.orchestration.coe.modeldefinition.ModelDescription.{RealType, Types}
+import org.slf4j.{Logger, LoggerFactory}
+
+class FMIGraphModel extends ModBatGraphModel {
+
+  // Instance vars
+  val logger: Logger = LoggerFactory.getLogger("FMIModel")
+
+  val fmuData = new FMUDynamicData()
+
+  var instance: IFmiComponent = _
+
+  // Clean up loaded instances
+  override def cleanupAfterException(e: Throwable): Unit = {
+    // Ignore certain exceptions that are normal part of modbat.
+    if (! e.isInstanceOf[RequirementFailedException]) {
+      logger.debug("Exception occurred: " + e)
+      if (instance != null) {
+        logger.debug("Freeing instance.")
+        instance.freeInstance()
+      }
+    }
+  }
+
+  def setInitialFMUData() = {
+    fmuData.promised_stop_time = -1.0
+    fmuData.time = 0.0
+  }
+
+  // Aux methods
+  def chooseRealVal(min: lang.Double, max: lang.Double, nominal: Double) = {
+    // TODO: Discretize interval and then choose one of the points non deterministically. Min and Max can be null
+    if (min == null || max == null){
+      1.0 // less likely to yield divisions by 0
+    } else {
+      assert(min != null && max != null)
+      nominal
+    }
+  }
+
+  def setReal(v: ModelDescription.ScalarVariable) = {
+    val realType = v.`type`.asInstanceOf[RealType]
+    val vl = chooseRealVal(realType.min, realType.max, realType.nominal)
+    FMUHandler.setReal(instance, v, vl)
+  }
+
+  def getReal(v: ModelDescription.ScalarVariable) = {
+    val realType = v.`type`.asInstanceOf[RealType]
+    FMUHandler.getReal(instance, v)
+  }
+
+
+  def setVar(v: ModelDescription.ScalarVariable) = {
+    require(v.`type`.`type` == Types.Real) // TODO: Implement setting for any var type
+    v.`type`.`type` match {
+      case Types.Boolean => throw new NotImplementedError()
+      case Types.Real => setReal(v)
+      case Types.String => throw new NotImplementedError()
+      case Types.Enumeration => throw new NotImplementedError()
+      case _ => assert(false)
+    }
+  }
+
+  def getVar(v: ModelDescription.ScalarVariable) = {
+    require(v.`type`.`type` == Types.Real) // TODO: Implement setting for any var type
+    v.`type`.`type` match {
+      case Types.Boolean => throw new NotImplementedError()
+      case Types.Real => getReal(v)
+      case Types.String => throw new NotImplementedError()
+      case Types.Enumeration => throw new NotImplementedError()
+      case _ => assert(false)
+    }
+  }
+
+  def getRandomElement[T](options: List[T]): T =  {
+    require(!options.isEmpty)
+    val i = choose(0, options.size)
+    val v = options(i)
+    v
+  }
+
+  // Edge Methods
+  def e_Instantiate() = {
+    logger.info("Instantiate: {}", FMUStaticData.fmuPath.getAbsolutePath)
+    instance = FMUHandler.instantiate(FMUStaticData.fmu, FMUStaticData.modDesc.getGuid)
+    setInitialFMUData()
+  }
+
+  def e_Reset() = {
+    FMUHandler.restart(instance)
+    setInitialFMUData()
+  }
+
+  def e_SetINI() = {
+    setVar(getRandomElement(FMUStaticData.INI))
+  }
+
+//  var e_GetTypesPlatformCount = 0
+  def e_GetTypesPlatform() = {
+//    require(e_GetTypesPlatformCount < 2)
+    assert(FMUStaticData.fmu.getTypesPlatform == "default")
+//    e_GetTypesPlatformCount += 1
+  }
+
+//  var e_GetVersionCount = 0
+  def e_GetVersion() = {
+//    require(e_GetVersionCount < 2)
+    assert(FMUStaticData.fmu.getVersion == "2.0")
+//    e_GetVersionCount += 1
+  }
+
+  def e_SetupExperiment() = {
+    choose(
+      () => {
+        fmuData.promised_stop_time = 1.0
+        instance.setupExperiment(false, 0.0, 0.0, true, fmuData.promised_stop_time)
+      },
+      () => {
+        instance.setupExperiment(false, 0.0, 0.0, false, fmuData.promised_stop_time)
+      }
+    )
+  }
+
+  def e_EnterInitMode() = {
+    val s = instance.enterInitializationMode()
+    assert(s == Fmi2Status.OK)
+  }
+
+  def e_SetIN() = {
+    setVar(getRandomElement(FMUStaticData.IN))
+  }
+
+  def e_SetINIE() = {
+    setVar(getRandomElement(FMUStaticData.INIE))
+  }
+
+  def e_GetINIT() = {
+    getVar(getRandomElement(FMUStaticData.INIT))
+  }
+
+  def e_ExitInitMode() = {
+    val s = instance.exitInitializationMode()
+    assert(s == Fmi2Status.OK)
+  }
+
+  def e_GetX() = {
+    getVar(getRandomElement(FMUStaticData.X))
+  }
+
+  def e_Terminate() = {
+    val s = instance.terminate()
+    assert(s == Fmi2Status.OK)
+  }
+
+  def e_Step() = {
+    // TODO: Consult default experiment
+    var H = 0.01
+    if (FMUStaticData.canHandleVarStep){
+      H = getRandomElement(List(0.001, 0.01, 0.1))
+    }
+    FMUHandler.doStep(instance, fmuData.time, H)
+    fmuData.time = fmuData.time+H
+  }
+
+  def e_Free() = {
+    instance.freeInstance()
+  }
+
+  def e_GetStepS() = {
+    val res = instance.getStatus(Fmi2StatusKind.DoStepStatus)
+    assert(res.status==Fmi2Status.OK)
+  }
+
+  def e_GetLST() = {
+    val res = instance.getRealStatus(Fmi2StatusKind.LastSuccessfulTime)
+    assert(res.status==Fmi2Status.OK)
+  }
+
+  def e_GetTermS() = {
+    // TODO: Need to solve https://github.com/INTO-CPS-Association/org.intocps.maestro.fmi/issues/7
+//    instance.getBooleanStatus(Fmi2StatusKind.Terminated)
+  }
+
+  def e_GetFmuState() = {
+    require(FMUStaticData.canGetSetState)
+    val res = instance.getState()
+    assert(res.status==Fmi2Status.OK)
+    val state = res.result
+    fmuData.snapshotState(state)
+  }
+
+  def e_SetFmuState() = {
+    require(FMUStaticData.canGetSetState)
+    require(fmuData.stateSnapshotExists)
+    val state = fmuData.restoreState()
+    val res = instance.setState(state)
+    assert(res==Fmi2Status.OK)
+  }
+
+  def e_FreeFmuState() = {
+    require(FMUStaticData.canGetSetState)
+    require(fmuData.stateSnapshotExists)
+    val res = fmuData.deleteStateSnapshot()
+    assert(res==Fmi2Status.OK)
+  }
+
+  loadTransitions()
+
+}

+ 99 - 0
src/main/scala/FMIModel.scala

@@ -0,0 +1,99 @@
+import modbat.dsl.Model
+import org.intocps.fmi.{Fmi2Status, IFmiComponent}
+import org.slf4j.{Logger, LoggerFactory}
+
+class FMIModel extends Model {
+
+  // Instance vars
+  val logger: Logger = LoggerFactory.getLogger("FMIModel")
+
+  var instance: IFmiComponent = _
+  var expDefined = false
+  var stopTime = -1.0
+
+  // Edge Methods
+
+  def e_Instantiate() = {
+    instance = FMUHandler.instantiate(FMUStaticData.fmu, FMUStaticData.modDesc.getGuid)
+  }
+
+  def e_GetTypes() = {
+    assert(FMUStaticData.fmu.getTypesPlatform == "default")
+  }
+
+  def e_GetVersion() = {
+    assert(FMUStaticData.fmu.getVersion == "2.0")
+  }
+
+  def e_SetType1Var() = {
+    require(!FMUStaticData.INI.isEmpty)
+    val i = choose(0, FMUStaticData.INI.size)
+    val v = FMUStaticData.INI(i)
+    FMUHandler.setReal(instance, v, 1.0)
+  }
+
+  def e_SetupExperiment() = {
+    require(!expDefined)
+    choose(
+      () => {
+        stopTime = 1.0
+        instance.setupExperiment(false, 0.0, 0.0, true, stopTime)
+      },
+      () => {
+        stopTime = -1.0
+        instance.setupExperiment(false, 0.0, 0.0, false, stopTime)
+      }
+    )
+    expDefined = true
+  }
+
+  def e_EnterInitMode() = {
+    require(expDefined)
+    val s = instance.enterInitializationMode()
+    assert(s == Fmi2Status.OK)
+  }
+
+  def e_SetType3Var() = {
+    require(!FMUStaticData.IN.isEmpty)
+    val i = choose(0, FMUStaticData.IN.size)
+    val v = FMUStaticData.IN(i)
+    FMUHandler.setReal(instance, v, 1.0)
+  }
+
+  def e_ExitInitMode() = {
+    val s = instance.exitInitializationMode()
+    assert(s == Fmi2Status.OK)
+  }
+
+  def e_Terminate() = {
+    val s = instance.terminate()
+    assert(s == Fmi2Status.OK)
+  }
+
+  def e_Free() = {
+    instance.freeInstance()
+  }
+
+  // Transitions
+
+  "loaded" -> "instantiated" := e_Instantiate()
+
+  "instantiated" -> "instantiated" := e_GetTypes()
+
+  "instantiated" -> "instantiated" := e_GetVersion()
+
+  "instantiated" -> "instantiated" := e_SetType1Var()
+
+  "instantiated" -> "instantiated" := e_SetupExperiment()
+
+  "instantiated" -> "initializing" := e_EnterInitMode()
+
+  "initializing" -> "initializing" := e_SetType3Var()
+
+  "initializing" -> "stepComplete" := e_ExitInitMode()
+
+  "stepComplete" -> "terminated" := e_Terminate()
+
+  "terminated" -> "freed" := e_Free()
+
+}

+ 105 - 0
src/main/scala/FMUAnalyzerApp.scala

@@ -0,0 +1,105 @@
+import java.io.File
+import java.math.BigInteger
+import java.net.{URL}
+
+import modbat.mbt.MBT
+import org.slf4j.LoggerFactory
+import scopt.OParser
+
+import scala.collection.mutable.ListBuffer
+
+object FMUAnalyzerApp extends App {
+
+  val logger = LoggerFactory.getLogger(getClass)
+
+  val APPROX = "approximation"
+  val SENSITIVITY = "sensitivity"
+  val MBTCMD = "mbt"
+
+  val builder = OParser.builder[Config]
+  val parser = {
+    import builder._
+    OParser.sequence(
+      programName("fmuanalyzer"),
+      head("fmuanalyzer", "0.0.1"),
+      opt[File]('d', "dir")
+        .required()
+        .valueName("<file>")
+        .action((x, c) => c.copy(dir = x))
+        .validate(f => {
+          if (!f.exists()) {
+            failure(s"Dir does not exist: $f")
+          } else {
+            success
+          }
+        })
+        .text("dir is a required directory or fmu"),
+      opt[String]('o', "outdir")
+        .action((x, c) => c.copy(outdir = x))
+        .text("Directory where results will be stored."),
+      help("help").text("prints this usage text"),
+      note("If dir is a directory, the program will run <command> for every fmu in that dir (recursively)." + sys.props("line.separator")),
+      cmd(APPROX)
+        .action((_, c) => c.copy(mode = APPROX))
+        .text("Try to guess input approximation scheme of FMU based on reference model.")
+        .children(
+          opt[String]('m', "model")
+            .required()
+            .valueName("<model>")
+            .action((x, c) => c.copy(model = x))
+            .validate(m => {
+              if (m != "Scalar") {
+                failure("Model must be 'Scalar'")
+              } else {
+                success
+              }
+            })
+            .text("model to be used as reference")
+        ),
+      cmd(SENSITIVITY)
+        .action((_, c) => c.copy(mode = SENSITIVITY))
+        .text("Compute sensitivity of the fmu."),
+      cmd(MBTCMD)
+        .action((_, c) => c.copy(mode = MBTCMD))
+        .text("Run Model Based Testing on the fmu.")
+        .children(
+          opt[Int]('n', s"number of runs per fmu")
+            .valueName("<nruns>")
+            .action((x, c) => c.copy(nruns = x))
+            .validate(n => {if (n <= 0) {failure("nruns > 0")} else {success}}),
+          opt[String]('s', "seed")
+            .action((x, c) => c.copy(seed = new BigInteger(x, 16)))
+            .text("Used to reproduce executions."),
+          opt[Unit]("stop")
+            .action((_, c) => c.copy(stopOnFailure = true))
+            .text("If a failure occurs, no more runs are done in the same FMU."),
+          opt[Unit]("delete-on-success")
+            .action((_, c) => c.copy(deleteOnSuccess = true))
+            .text("When the tests success, delete logs."),
+          opt[Int]('l', "max self loop executions per state")
+            .action((x, c) => c.copy(loopLimit = x))
+            .text("The same state will be visited at most x times, with x<l. Default is l=0 (no limit). If l=1, then no self loops are executed.")
+            .validate(n => {if (n < 0) { failure("nruns >= 0") } else { success } })
+        )
+    )
+  }
+
+  val urls = ListBuffer[URL]()
+  MBT.recCollectClassLoaderURLs(getClass.getClassLoader, urls)
+  logger.debug("Classpath used:")
+  urls.foreach(u => logger.debug(u.toString))
+
+  // OParser.parse returns Option[Config]
+  OParser.parse(parser, args, Config()) match {
+    case Some(config) =>
+      config.mode match {
+        case APPROX => FMUHandler.applyCmdFMUsDir(config.dir, (f) => InputApproximationAnalyzer.guessInputApproximationsFmu(f, config.model))
+        case SENSITIVITY => FMUHandler.applyCmdFMUsDir(config.dir, (f) => SensitivityAnalyzer.computeSensitivity(f, 0.01, false))
+        case MBTCMD => FMUHandler.applyCmdFMUsDir(config.dir, (f) => MBTRunner.run_mbt(f, config, classOf[FMIGraphModel]))
+      }
+    case _ =>
+    // arguments are bad, error message will have been displayed
+  }
+
+
+}

+ 32 - 0
src/main/scala/FMUDynamicData.scala

@@ -0,0 +1,32 @@
+import org.intocps.fmi.IFmiComponentState
+
+class FMUDynamicData {
+  // Accessed by State Machine Model
+  var time: Double = _
+  var promised_stop_time: Double = _
+  private var internalState: IFmiComponentState = null
+  private var stateTime: Double = _
+
+  def snapshotState(state: IFmiComponentState): Unit = {
+    internalState = state
+    stateTime = time
+  }
+
+  def stateSnapshotExists() = {
+    internalState != null
+  }
+
+  def deleteStateSnapshot() = {
+    val res = internalState.freeState()
+    internalState = null
+    stateTime = -1
+    res
+  }
+
+  def restoreState() = {
+    time = stateTime
+    internalState
+  }
+
+
+}

+ 130 - 0
src/main/scala/FMUHandler.scala

@@ -0,0 +1,130 @@
+import java.io.File
+
+import org.apache.commons.io.FilenameUtils
+import org.intocps.fmi.jnifmuapi.Factory
+import org.intocps.fmi.{Fmi2Status, FmuInvocationException, IFmiComponent, IFmu, IFmuCallback}
+import org.intocps.orchestration.coe.modeldefinition.ModelDescription
+import org.slf4j.{Logger, LoggerFactory}
+
+object FMUHandler {
+
+  val logger: Logger = LoggerFactory.getLogger(getClass)
+
+  def isFMUFile(f: File): Boolean = {
+    FilenameUtils.getExtension(f.getName) == "fmu"
+  }
+
+  var loggingOn = true
+
+  def doStep(instance: IFmiComponent, t: Double, H: Double) = {
+    val res = instance.doStep(t, H, true)
+    assert(res == Fmi2Status.OK)
+  }
+
+  def setReal(instance: IFmiComponent, v: ModelDescription.ScalarVariable, value: Double) = {
+    logger.debug(s"Setting real ${v.name} (reference ${v.valueReference}) = $value.")
+    val res = instance.setReals(Array[Long](v.valueReference), Array[Double](value))
+    assert(res == Fmi2Status.OK)
+  }
+
+  def getReal(instance: IFmiComponent, v: ModelDescription.ScalarVariable) = {
+    val status = instance.getReal(Array[Long](v.valueReference))
+    assert(status.status == Fmi2Status.OK)
+    status.result(0)
+  }
+
+  def instantiate(fmu: IFmu, guid: String) = {
+    val instance = fmu.instantiate(guid, "fmu", true, loggingOn, new IFmuCallback() {
+      override def log(instanceName: String, status: Fmi2Status, category: String, message: String): Unit = {
+        val msg = instanceName + ", " + status + ", " + category + ", " + message
+        status match {
+          case Fmi2Status.OK =>
+            logger.debug(msg)
+          case Fmi2Status.Warning =>
+            logger.warn(msg)
+          case Fmi2Status.Error =>
+            logger.error(msg)
+          case Fmi2Status.Fatal =>
+            logger.error(msg)
+          case _ =>
+            logger.warn(msg)
+        }
+      }
+      override def stepFinished(fmi2Status: Fmi2Status): Unit = {
+      }
+    })
+    assert(instance != null)
+    instance
+  }
+
+  def recGetFMUs(dir: File): Array[File] = {
+    val filesList = dir.listFiles
+    val allFiles = filesList ++ filesList.filter(_.isDirectory).flatMap(recGetFMUs)
+    allFiles.filter(isFMUFile)
+  }
+
+  def printFmuCapabilities(modDesc: ModelDescription) = {
+    logger.debug("FMU Capabilities: ")
+    logger.debug("Get/Set State: {}", modDesc.getCanGetAndSetFmustate)
+    logger.debug("Variable Step Size: {}", modDesc.getCanHandleVariableCommunicationStepSize)
+    logger.debug("Interpolate Inputs: {}", modDesc.getCanInterpolateInputs)
+  }
+
+  def load(file: File): (IFmu, ModelDescription) = {
+    assert(file.exists())
+    val fmu: IFmu = Factory.create(file)
+    fmu.load()
+    val modDesc = new ModelDescription(fmu.getModelDescription())
+
+    printFmuCapabilities(modDesc)
+
+    (fmu, modDesc)
+  }
+
+  def load(path: String): (IFmu, ModelDescription) = {
+    val fmuFile = new File(path)
+    load(fmuFile)
+  }
+
+
+  def initialize(instance: IFmiComponent) = {
+    assert(instance.setupExperiment(false, 0.0, 0.0, false, 0.0) == Fmi2Status.OK)
+    assert(instance.enterInitializationMode() == Fmi2Status.OK)
+    // No initial unknowns
+    assert(instance.exitInitializationMode() == Fmi2Status.OK)
+  }
+
+  @throws[FmuInvocationException]
+  def restart(instance: IFmiComponent) = {
+    assert(instance.reset() == Fmi2Status.OK)
+  }
+
+  def terminate(instance: IFmiComponent) = {
+    assert(instance.terminate() == Fmi2Status.OK)
+  }
+
+  def reinit(instance: IFmiComponent) = {
+    restart(instance)
+    initialize(instance)
+  }
+
+  def cleanup(fmu: IFmu) : Unit = {
+    fmu.unLoad
+  }
+
+  def applyCmdFMUsDir(dir: File, cmd: File => Unit) = {
+    assert(dir.exists(), s"Path given does not exist: $dir")
+    if (dir.isDirectory) {
+      val fmus = FMUHandler.recGetFMUs(dir).toList
+      logger.info(s"FMUs found in dir $dir: ${fmus.size}")
+      for (f <- fmus) {
+        cmd(f)
+      }
+    } else {
+      assert(dir.isFile)
+      assert(FMUHandler.isFMUFile(dir))
+      cmd(dir)
+    }
+  }
+
+}

+ 54 - 0
src/main/scala/FMUStaticData.scala

@@ -0,0 +1,54 @@
+import java.io.File
+
+import org.intocps.fmi.IFmu
+import org.intocps.orchestration.coe.modeldefinition.ModelDescription
+import org.intocps.orchestration.coe.modeldefinition.ModelDescription._
+
+import scala.collection.JavaConverters._
+
+object FMUStaticData {
+  // Accessed by State Machine Model
+  var fmu: IFmu = _
+  var INI: List[ScalarVariable] = _
+  var IN: List[ScalarVariable] = _
+  var INIE: List[ScalarVariable] = _
+  var INIT: List[ScalarVariable] = _
+  var X: List[ScalarVariable] = _
+  var modDesc: ModelDescription = _
+  var canHandleVarStep: Boolean = _
+  var canGetSetState: Boolean = _
+  var fmuPath : File = _
+
+  def initData(fm: IFmu, m: ModelDescription, path: File) = {
+    fmu = fm
+    modDesc = m
+    fmuPath = path
+
+    val vars = modDesc.getScalarVariables.asScala.toList
+    val ders = modDesc.getDerivatives.asScala.toList
+    val derMap = modDesc.getDerivativesMap
+
+    INI = vars.filter(v =>
+      (v.variability != Variability.Constant && (v.initial == Initial.Approx || v.initial == Initial.Exact)) ||
+        (ders.contains(v) && v.`type`.`type` == Types.Real && v.causality == Causality.Input))
+
+    IN = vars.filter(v =>
+      v.causality==Causality.Input ||
+        (v.causality != Causality.Parameter && v.variability == Variability.Tunable))
+
+    INIE = vars.filter(v =>
+      v.causality==Causality.Input &&
+        (v.variability != Variability.Constant && v.initial == Initial.Exact))
+
+    INIT = vars.filter(v =>
+      v.causality==Causality.Output ||
+      ders.contains(v) ||
+      derMap.containsValue(v))
+
+    // TODO: We also need directional derivatives.
+    X = vars.filter(v => v.causality==Causality.Output)
+
+    canHandleVarStep = modDesc.getCanHandleVariableCommunicationStepSize
+    canGetSetState = modDesc.getCanGetAndSetFmustate
+  }
+}

+ 5 - 0
src/main/scala/FmuInvocationStatusException.scala

@@ -0,0 +1,5 @@
+import org.intocps.fmi.{Fmi2Status, FmuInvocationException}
+
+case class FmuInvocationStatusException(instanceName: String, status: Fmi2Status, category: String, message: String) extends FmuInvocationException(message) {
+
+}

+ 239 - 0
src/main/scala/GraphMLLoader.scala

@@ -0,0 +1,239 @@
+import java.io.{BufferedReader, FileNotFoundException, InputStream, InputStreamReader}
+import java.nio.charset.StandardCharsets
+
+import scala.collection.JavaConversions._
+import org.apache.commons.io.FilenameUtils
+import scalax.collection.Graph
+import scalax.collection.edge.LkDiEdge
+import scalax.collection.GraphPredef._
+import ImplicitEdgeConverter._
+import org.slf4j.{Logger, LoggerFactory}
+
+import scala.collection.mutable.ListBuffer
+import scala.util.matching.Regex
+import scala.xml.{Elem, XML}
+
+object GraphMLLoader {
+
+  val logger: Logger = LoggerFactory.getLogger(getClass)
+
+  def getNodeLabel(e: Elem) = {
+    val labels = (e \\ "NodeLabel")
+    if (labels.isEmpty){
+      ""
+    } else {
+      val labelElement = labels.head.asInstanceOf[Elem]
+      labelElement.text.trim
+    }
+  }
+
+  def getEdgeLabel(e: Elem) = {
+    val labels = (e \\ "EdgeLabel")
+    if (labels.isEmpty) {
+      ""
+    } else {
+      val labelElement = labels.head.asInstanceOf[Elem]
+      labelElement.text.trim
+    }
+  }
+
+  def transformSingleNode(e: Elem): NodeData = {
+    val id = (e \@ "id")
+    val label = getNodeLabel(e)
+    FlatNodeData(id, label)
+  }
+
+  def transformHierarchicalNode(e: Elem, rootGraph: Elem): NodeData = {
+    val id = (e \@ "id")
+    val label = getNodeLabel(e)
+    val innerGraphSrc = (e \ "graph").head.asInstanceOf[Elem]
+    assert((innerGraphSrc \ "edge").isEmpty, "We assume that there are no edges in the child graph.")
+    val child = transformGraph(innerGraphSrc, rootGraph)
+    HierarchicalNodeData(id, label, child)
+  }
+
+  def transformNode(e: Elem, rootGraph: Elem): NodeData = {
+    val hasInnerGraph = !(e \ "graph").isEmpty
+    if (hasInnerGraph){
+      transformHierarchicalNode(e, rootGraph)
+    } else {
+      transformSingleNode(e)
+    }
+  }
+
+  def transformEdge(e: Elem, nodes: Map[String, NodeData]) = {
+    val id = (e \@ "id")
+    val label = getEdgeLabel(e)
+    val srcId = (e \@ "source")
+    val trgId = (e \@ "target")
+
+    if (nodes.contains(srcId) && nodes.contains(trgId)){
+      LkDiEdge(nodes(srcId), nodes(trgId))(EdgeData(id, label))
+    } else if (!nodes.contains(srcId) && !nodes.contains(trgId)) {
+      // Edge does not belong in this graph, but instead is part of a child graph, or a parent graph.
+      null
+    } else {
+      throw new NotImplementedError(s"Found edge that is crossing between graphs at different levels: $e")
+    }
+  }
+
+  def transformGraph(srcGraph: Elem, rootGraph: Elem) = {
+    val nodesMap = (srcGraph \ "node").map(n => {
+      val e = n.asInstanceOf[Elem]
+      val id = (e \@ "id")
+      id -> transformNode(e, rootGraph)
+    }).toMap
+    // Use rootgraph to query edges because edges only exist in the root graph.
+    val edges = (rootGraph \ "edge").map(n => {
+      val e = n.asInstanceOf[Elem]
+      transformEdge(e, nodesMap)
+    }).filter(_ != null)
+
+    Graph.from(nodesMap.values.toList, edges.toList)
+  }
+
+  def expandSugarEdge(src: NodeData, trg: NodeData, id: String, action: String) = {
+    val actions = action.split('|').map(_.trim)
+
+    actions.indices
+      .map(i => {
+        val (action, bound) = extractActionBound(actions(i))
+        LkDiEdge(src, trg)(EdgeData(id + "::" + i, action, bound))
+      })
+      .toList
+  }
+
+  def expandSugarEdges(graph : Graph[NodeData, LkDiEdge]) : Graph[NodeData, LkDiEdge] = {
+    val edges = graph.edges.toList.flatMap(e => {
+      expandSugarEdge(e.source.value, e.target.value, e.id, e.action)
+    })
+
+    Graph.from(graph.nodes, edges)
+  }
+
+  def extractActionBound(label : String) = {
+    val boundPattern: Regex = "\\[\\d+\\]".r
+
+    boundPattern.findFirstMatchIn(label) match {
+      case Some(theMatch) => (theMatch.before.toString.trim(), theMatch.matched.slice(1, theMatch.matched.size - 1).toInt)
+      case None => (label, Int.MaxValue)
+    }
+  }
+
+  def expandNode(node : NodeData): Seq[(String, String)] = {
+    node match {
+      case FlatNodeData(id, label) => expandNode(id, label)
+      case HierarchicalNodeData(id, label, _) => expandNode(id, label)
+    }
+  }
+
+  def expandNode(id : String, label : String): Seq[(String, String)] = {
+    val labels = label.split('|').map(_.trim)
+    labels.indices.map(i => ((if (i == 0) id else id + "-" + i), labels(i)))
+  }
+
+  def expandFlatNode(id : String, label : String) = {
+    val tuples = expandNode(id, label)
+    tuples.indices.map(i => tuples(i) -> FlatNodeData(tuples(i)._1, tuples(i)._2))
+  }
+
+  def expandHierarchicalNode(id : String, label : String, child : Graph[NodeData, LkDiEdge]) = {
+    val tuples = expandNode(id, label)
+    tuples.indices.map(i => tuples(i) -> HierarchicalNodeData(tuples(i)._1, tuples(i)._2, child))
+  }
+
+  def expandSugarNodes(graph : Graph[NodeData, LkDiEdge]): Graph[NodeData, LkDiEdge] = {
+    val edges = ListBuffer[LkDiEdge[NodeData]]()
+    val nodesMap = graph.nodes.flatMap(node => {
+      node.value match {
+        case FlatNodeData(id, label) => expandFlatNode(id, label)
+        case HierarchicalNodeData(id, label, child) => expandHierarchicalNode(id, label, expandSugarNodes(child))
+      }
+    }).toMap
+
+    for (edge <- graph.edges) {
+      val srcKeys = expandNode(edge.source.value)
+      val trgKeys = expandNode(edge.target.value)
+
+      srcKeys.foreach(sk => {
+        trgKeys.foreach(tk => {
+          // FIXME: new edge IDs
+          edges += LkDiEdge(nodesMap(sk), nodesMap(tk))(EdgeData(edge.id, edge.action, edge.bound))
+        })
+      })
+    }
+
+    Graph.from(nodesMap.values.toList, edges.toList)
+  }
+
+
+  def nodeLabel(node : NodeData): String = {
+    node match {
+      case FlatNodeData(_, label) => label
+      case HierarchicalNodeData(_, label, _) => label
+    }
+  }
+
+  // FIXME: Duplicate edge actions are not checked during merge.
+  def mergeGraphs(lhs : Graph[NodeData, LkDiEdge], rhs : Graph[NodeData, LkDiEdge]) : Graph[NodeData, LkDiEdge] = {
+    val lhsNodesMap = lhs.nodes.map(n => nodeLabel(n.value) -> n.value).toMap
+    val rhsNodesMap = rhs.nodes.map(n => nodeLabel(n.value) -> n.value).toMap
+
+    val nodesMap = (lhsNodesMap ++ rhsNodesMap).map {
+      case (label, rhsNode) => {
+        rhsNode match {
+          case FlatNodeData(_, _) => label -> rhsNode
+          case HierarchicalNodeData(id, _, rChild) =>
+            val lChild = lhsNodesMap(label).asInstanceOf[HierarchicalNodeData].child
+            label -> HierarchicalNodeData(id, label, mergeGraphs(lChild, rChild))
+        }
+      }
+    }
+
+    val edgesMap = collection.mutable.Map[(String, String, String), LkDiEdge[NodeData]]()
+
+    (lhs.edges.toList ::: rhs.edges.toList).foreach(e => {
+      val sk = nodeLabel(e.source.value)
+      val tk = nodeLabel(e.target.value)
+      val k = (e.action, sk, tk)
+
+      if (edgesMap contains k) {
+        val id = edgesMap(k).label.id
+        edgesMap(k) = LkDiEdge(nodesMap(sk), nodesMap(tk))(EdgeData(id + "::" + e.id , e.action, e.bound))
+      } else {
+        edgesMap(k) = LkDiEdge(nodesMap(sk), nodesMap(tk))(EdgeData(e.id, e.action, e.bound))
+      }
+    })
+
+    Graph.from(nodesMap.values.toList, edgesMap.values.toList)
+  }
+
+  def createGraph(file: InputStream): Graph[NodeData, LkDiEdge] = {
+    val contents = XML.load(file)
+    val graphML = (contents \ "graph").head.asInstanceOf[Elem]
+    val graph = transformGraph(graphML, graphML)
+
+    expandSugarEdges(
+      expandSugarNodes(
+        graph
+      )
+    )
+  }
+
+  def createGraphFromPath(paths : Traversable[String]): Option[Graph[NodeData, LkDiEdge]] = {
+    // Reading the contents of a folder in a jar seems to be a big issue, so the hacks are in a separate object
+    Some(paths
+      .filter(f => FilenameUtils.getExtension(f) == "graphml")
+      .map(file => {
+        logger.debug(s"Creating graph for file ${file}")
+        val stream = getClass.getResourceAsStream(file)
+        if (stream == null) {
+          throw new FileNotFoundException(s"Resource ${file} not found.")
+        }
+        createGraph(stream)
+      })
+      .reduce((g1, g2) => mergeGraphs(g1, g2))
+    )
+  }
+
+}

+ 11 - 0
src/main/scala/GraphMLTypes.scala

@@ -0,0 +1,11 @@
+import scalax.collection.Graph
+import scalax.collection.edge.LBase.LEdgeImplicits
+import scalax.collection.edge.LkDiEdge
+
+object ImplicitEdgeConverter extends LEdgeImplicits[EdgeData];
+
+abstract class NodeData(id: String, label: String)
+case class FlatNodeData(id: String, label: String) extends NodeData(id, label)
+case class HierarchicalNodeData(id: String, label: String, child: Graph[NodeData, LkDiEdge]) extends NodeData(id, label)
+
+case class EdgeData(id: String, action: String, bound: Int = Int.MaxValue)

+ 114 - 0
src/main/scala/InputApproximationAnalyzer.scala

@@ -0,0 +1,114 @@
+import java.io.File
+
+import breeze.linalg.{DenseVector, sum}
+import breeze.numerics.pow
+import breeze.plot.{Plot, plot}
+import org.intocps.fmi.{IFmiComponent, IFmu}
+import org.intocps.orchestration.coe.modeldefinition.ModelDescription
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+object InputApproximationAnalyzer {
+
+  def guessInputApproximationsFmu(f: File, model: String) = {
+    val (fmu, modDesc) = FMUHandler.load(f)
+    model match {
+      case "Scalar" => {
+        val (bestOrder, minError) = findInputApproxOrder(fmu, modDesc, new ScalarFMUModel)
+        println(s"Best guess: $bestOrder (error=$minError).")
+      }
+      case _ => assert(false, "Model must be 'Scalar'.")
+    }
+    FMUHandler.cleanup(fmu)
+  }
+
+  def sos(v1:DenseVector[Double], v2:DenseVector[Double]):Double = {
+    val m = v1-v2
+    val p = pow(m, 2)
+    sum(p)
+  }
+
+
+  def lagrangePolynomials(tc:Double, n: Int, H: Double, u: Double => Double)={
+    val basis = (i:Int, t:Double) => (0 to n).map(j => if(j != i) (t - (tc-j*H))/((tc - i*H) - (tc-j*H)) else 1.0).reduce((v1,v2)=>v1*v2)
+    val res = (t:Double) => (0 to n).map(i => u(tc - i*H)*basis(i, t)).sum
+    res
+  }
+
+  def runScalarInApproxExperiment(order: Int, modDesc: ModelDescription, instance: IFmiComponent, model: ScalarFMUModel, p:Option[Plot]) = {
+    val vars = modDesc.getScalarVariables.asScala
+    val uVar = vars.find(_.name == "u").get
+    val xVar = vars.find(_.name == "x").get
+
+    val u = (t:Double) => if (t<1.0) 0.0 else 1.0
+    var uApprox = (th:Double) => u(0)
+    val H = 0.1
+    var t = 0.0
+
+    val xsFmu = mutable.MutableList[Double]()
+    val xsMod = mutable.MutableList[Double]()
+    val ts = mutable.MutableList[Double]()
+    val tsU = mutable.MutableList[Double]()
+    val uInts = mutable.MutableList[Double]()
+    val uExts = mutable.MutableList[Double]()
+
+    while (t<=2.0) {
+
+      xsFmu += FMUHandler.getReal(instance, xVar)
+      xsMod += model.x
+      ts += t
+      uExts += u(t)
+      uApprox = (th:Double) => lagrangePolynomials(t, order, H, u)(th) // Constant extrapolation
+
+      var tn = t
+      while (tn < t+H){
+        tsU += tn
+        uInts += uApprox(tn)
+        tn += H/10.0
+      }
+
+      FMUHandler.setReal(instance, uVar, u(t))
+
+      FMUHandler.doStep(instance, t, H)
+      model.doStep(t, H, uApprox)
+
+      t += H
+    }
+
+    p match {
+      case Some(p) => {
+        p += plot(ts, xsFmu, '.', name=s"xFmu(n=$order)")
+        p += plot(ts, xsMod, style='.', name=s"xMod(n=$order)")
+        p += plot(tsU, uInts, '.', name=s"uInt(n=$order)")
+        p += plot(ts, uExts, '.', name=s"u")
+      }
+      case None => {}
+    }
+
+    val xsFmuVector = new DenseVector[Double](xsFmu.toArray)
+    val xsModVector = new DenseVector[Double](xsMod.toArray)
+
+    (ts, xsFmuVector, xsModVector)
+  }
+
+  def findInputApproxOrder(fmu:IFmu, modDesc:ModelDescription, model:ScalarFMUModel) = {
+    val MAX_ORDER = 5
+    var bestOrder = 0
+    var minError = Double.MaxValue
+    for (order <- 0 to MAX_ORDER){
+      val instance = FMUHandler.instantiate(fmu, modDesc.getGuid)
+      FMUHandler.initialize(instance)
+      model.reset()
+      val (ts, xsFmu, xsMod) = runScalarInApproxExperiment(order, modDesc, instance, model, None)
+      val error = sos(xsFmu, xsMod)
+      if (error < minError) {
+        bestOrder = order
+        minError = error
+      }
+      FMUHandler.terminate(instance)
+    }
+    model.reset()
+    (bestOrder, minError)
+  }
+}

+ 155 - 0
src/main/scala/MBTRunner.scala

@@ -0,0 +1,155 @@
+import java.io.File
+import java.math.BigInteger
+import java.nio.file.{FileSystemException, Path}
+
+import modbat.log.Log
+import modbat.mbt.Modbat.{AppState, runTests}
+import modbat.mbt.{Dotify, MBT, Modbat}
+import org.intocps.fmi.IFmu
+import org.intocps.orchestration.coe.modeldefinition.ModelDescription
+import org.slf4j.{Logger, LoggerFactory}
+
+object MBTRunner {
+
+  val logger = LoggerFactory.getLogger(getClass)
+
+  def translateLogger(): Int ={
+    val logger = LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME)
+    if (logger.isTraceEnabled)
+      return Log.All
+    if (logger.isDebugEnabled())
+      return Log.Debug
+    if (logger.isInfoEnabled())
+      return Log.Info
+    if (logger.isWarnEnabled())
+      return Log.Warning
+    if (logger.isErrorEnabled())
+      return Log.Error
+    return Log.All
+  }
+
+  def getModbatResults() = {
+    val executed = Modbat.count
+    val failed = Modbat.failed
+
+    val failures = Modbat.testFailures
+
+    ModBatResults(executed, failed, failures)
+  }
+
+  def checkAndCreate(d: File) = {
+    if (d.exists() && !d.isDirectory) {
+      throw new IllegalArgumentException("Results directory must be a directory: " + d.getAbsolutePath)
+    }
+
+    var sufix = 0 // used to ensure a new dir is created per fmu.
+    var f = new File(d, s"_$sufix")
+    while(f.exists()) {
+      logger.debug("Results directory already exists: {}. Attempting new name.", f)
+      sufix += 1
+      f = new File(d, s"_$sufix")
+    }
+
+    logger.debug("Creating results directory: {}", f)
+    if (!f.mkdirs()) {
+      throw new FileSystemException(s"Failed to create results directory: $f")
+    }
+
+    f
+  }
+
+  def getResultsDir(outdir: String, fm: IFmu, m: ModelDescription) = {
+    var toolDesc = m.getGenerationTool.replaceAll("\\s+", "")
+    if (toolDesc == "") {
+      toolDesc = "generic"
+    }
+    if (toolDesc.length > 15) {
+      toolDesc = toolDesc.substring(0,15)
+    }
+    new File(new File(outdir, toolDesc), m.getModelId)
+  }
+
+  def cleanupResults(results_out_dir: File): Unit = {
+    if (results_out_dir.list().isEmpty) {
+      logger.debug("Deleting dir {} as it is empty.", results_out_dir)
+      val success = results_out_dir.delete()
+      if (!success) {
+        logger.warn("Failed to delete empty dir {}", results_out_dir)
+      }
+    }
+  }
+
+  def run_mbt(f: File, config: Config, modelClass: Class[_]) = {
+    logger.info("Running modbat on fmu {}", f)
+
+    val (fm,m) = FMUHandler.load(f)
+
+    // Use single dir per fmu, for easier identification of problems
+    val results_out_dir = checkAndCreate(getResultsDir(config.outdir, fm, m))
+
+    try{
+      FMUStaticData.initData(fm, m, f)
+
+      val mbtConfig = modbat.mbt.Main.config
+
+      if (config.seed != null) {
+        logger.debug("Seed specified: " + config.seed)
+        mbtConfig.randomSeed = config.seed.longValue()
+      } else {
+        // Default seed is both current time and a hash of the fmu path.
+        // This is because many repeated tests in parallel can actually create equal seeds.
+        mbtConfig.randomSeed = (System.nanoTime(), f.hashCode()).hashCode()
+        logger.debug("Using default seed: " + mbtConfig.randomSeed.toHexString)
+      }
+
+      mbtConfig.loopLimit = config.loopLimit
+      mbtConfig.stopOnFailure = config.stopOnFailure
+      //    config.dotifyCoverage = true
+      //    config.dotifyPathCoverage = true
+      mbtConfig.nRuns = config.nruns
+      mbtConfig.logLevel = translateLogger
+      mbtConfig.redirectOut = true
+      mbtConfig.logPath = results_out_dir.getAbsolutePath
+      mbtConfig.removeLogOnSuccess = config.deleteOnSuccess
+      MBT.configClassLoader(mbtConfig.classpath)
+      MBT.modelClass = modelClass
+      Log.setLevel(mbtConfig.logLevel)
+      MBT.setRNG(mbtConfig.randomSeed)
+      MBT.isOffline = false
+
+
+      Modbat.init
+      Modbat.runTests(mbtConfig.nRuns)
+
+      val report = getModbatResults()
+
+      Modbat.shutdown
+      Modbat.appState = AppState.AppShutdown
+
+      Modbat.coverage
+
+      report
+    } finally {
+      FMUHandler.cleanup(fm)
+      cleanupResults(results_out_dir)
+    }
+  }
+
+  def draw_fmi_model() = {
+    val modelClassName = classOf[FMIGraphModel].getName
+    val config = modbat.mbt.Main.config
+    config.classpath = "./target/scala-2.11/classes;."
+    config.stopOnFailure = true
+    config.logLevel = translateLogger
+    config.redirectOut = false
+    MBT.configClassLoader(config.classpath)
+    MBT.loadModelClass(modelClassName)
+    Log.setLevel(config.logLevel)
+    MBT.setRNG(config.randomSeed)
+    MBT.isOffline = false
+    val launch = MBT.launch(null)
+    val dotify = new Dotify(launch, modelClassName + ".dot")
+    dotify.dotify()
+  }
+
+}

+ 95 - 0
src/main/scala/ModBatGraphModel.scala

@@ -0,0 +1,95 @@
+import java.io.FileNotFoundException
+import java.lang.reflect.{InvocationTargetException, Method}
+
+import ImplicitEdgeConverter._
+import modbat.dsl._
+
+import scala.collection.mutable
+
+abstract class ModBatGraphModel extends Model {
+
+  def getMethodByName(name: String) = getClass.getMethod(name)
+
+  def extractData(e: EdgeData) = {
+    val method = if (!e.action.isEmpty) getMethodByName(e.action) else null
+    (e.id, method)
+  }
+
+  def cleanupAfterException(e: Throwable): Unit
+
+  val _invocationsTable = new mutable.HashMap[String, Int]
+
+  def checkedInvoke(id: String, m: Method, obj: ModBatGraphModel, bound: Int) = {
+    val invocations = if (_invocationsTable.contains(id)) _invocationsTable(id) else 0
+
+    // Ensures backtracking of path generation algorithm.
+    require(invocations < bound, s"Maximum number of invocations exceeded for transition ${id}")
+
+    try {
+      m.invoke(obj)
+      _invocationsTable(id) = invocations + 1
+    } catch {
+      case e: InvocationTargetException => {
+        cleanupAfterException(e.getCause)
+        throw e.getCause
+      }
+    }
+  }
+
+  def transitionLabel(srcLbl: String, trgLbl: String, method: Method): String = s"$srcLbl-${method.getName}->$trgLbl"
+
+  def generate_edge_id(action: String, srcLbl: String, trgLbl: String): String = {
+    val id = srcLbl+"-"+action+"->"+trgLbl
+    id
+  }
+
+  def validateTransitionIds(transitions: List[(String, String, String, Method, Int)]) = {
+    for ((srcLbl, trgLbl, id, method, bound) <- transitions) {
+      assert(transitions.filter( tr => tr._3 == id ).size==1, s"Duplicate transition found: ${id}.")
+    }
+  }
+
+  def loadTransitions() = {
+    // Load graphml and translate graph to modbat dsl graph.
+
+    // For now we have to hardcode the path to the files that need to be merged. This is because its hard to list folder contents inside a jar.
+    // See https://stackoverflow.com/questions/11012819/how-can-i-get-a-resource-folder-from-inside-my-jar-file
+    // and https://stackoverflow.com/questions/28985379/java-how-to-read-folder-and-list-files-in-that-folder-in-jar-environment-instead
+    val graph = GraphMLLoader.createGraphFromPath(
+                                  List("/graphs/fmi/fmi.graphml",
+                                    "/graphs/fmi/free.graphml",
+                                    "/graphs/fmi/reset.graphml"))
+
+    if (! graph.isDefined){
+      throw new FileNotFoundException("Could not load graphs in resource /graphs/fmi/")
+    }
+    val intGraph = graph.get
+
+    val transitions = intGraph.edges.map(e => {
+      val srcLbl = e.source.value.asInstanceOf[FlatNodeData].label
+      val trgLbl = e.target.value.asInstanceOf[FlatNodeData].label
+      val eId = generate_edge_id(e.action, srcLbl, trgLbl)
+      (srcLbl, trgLbl, eId, getMethodByName(e.action), e.bound)
+    }).toList
+
+    validateTransitionIds(transitions)
+
+    val that = this
+
+    // Make sure first transition is the one leaving start state, as that is the initial state.
+    val (str_srcLbl, str_trgLbl, str_id, method, int_bound) = transitions.find(_._1 == "start").get
+    // add initial transition
+    str_srcLbl -> str_trgLbl := {
+      checkedInvoke(str_id, method, that, int_bound)
+    } label transitionLabel(str_srcLbl, str_trgLbl, method)
+
+    // add remaining transitions
+    for ((srcLbl, trgLbl, id, method, bound) <- transitions) {
+      if (id != str_id) {
+        srcLbl -> trgLbl := {
+          checkedInvoke(id, method, that, bound)
+        } label transitionLabel(srcLbl: String, trgLbl: String, method: Method)
+      }
+    }
+  }
+}

+ 8 - 0
src/main/scala/ModBatResults.scala

@@ -0,0 +1,8 @@
+import modbat.trace.TransitionResult
+
+import scala.collection.mutable
+import scala.collection.mutable.ListBuffer
+
+case class ModBatResults(executed: Int, failed: Int, failures: mutable.HashMap[(TransitionResult, String), ListBuffer[Long]]) {
+
+}

+ 15 - 0
src/main/scala/ScalarFMUModel.scala

@@ -0,0 +1,15 @@
+import breeze.integrate._
+
+class ScalarFMUModel {
+  var x = 1.0
+  reset()
+
+  def reset(): Unit ={
+    x = 1.0
+  }
+
+  def doStep(t:Double, H:Double, u: Double => Double): Unit = {
+    x = x + trapezoid(u, t, t+H, 100)
+  }
+
+}

+ 96 - 0
src/main/scala/SensitivityAnalyzer.scala

@@ -0,0 +1,96 @@
+import java.io.File
+
+import breeze.linalg.isClose
+import org.intocps.fmi.IFmu
+import org.intocps.orchestration.coe.modeldefinition.ModelDescription
+import org.intocps.orchestration.coe.modeldefinition.ModelDescription.{Causality, ScalarVariable}
+
+import scala.collection.JavaConverters._
+import scala.collection.mutable
+
+object SensitivityAnalyzer {
+
+  def computeDisturbance(inVar: ScalarVariable) = 1.0
+
+  def computeSensitivity(f: File, H: Double, allVars:Boolean): Unit ={
+    val (fmu, modDesc) = FMUHandler.load(f)
+    val (instantSensitivityMatrix, stepSensitivityMatrix) = computeSensitivity(fmu, modDesc, H, allVars)
+    printSensitivityResults(f.getPath, instantSensitivityMatrix, stepSensitivityMatrix, H)
+    FMUHandler.cleanup(fmu)
+  }
+
+  def computeSensitivity(fmu: IFmu, modDesc:ModelDescription, H: Double, allVars:Boolean) = {
+    var instance = FMUHandler.instantiate(fmu, modDesc.getGuid)
+    FMUHandler.initialize(instance)
+
+    val vars = modDesc.getScalarVariables.asScala
+    val inVars = vars.filter(_.causality == Causality.Input).toList
+    val outVars = vars.filter(if (allVars) _.causality != Causality.Input else _.causality == Causality.Output).toList
+    val instantSensitivityMatrix = new mutable.HashMap[ScalarVariable, mutable.HashMap[ScalarVariable, Double]]()
+    val stepSensitivityMatrix = new mutable.HashMap[ScalarVariable, mutable.HashMap[ScalarVariable, Double]]()
+
+    for (inVar <- inVars) {
+      instantSensitivityMatrix.put(inVar, new mutable.HashMap[ScalarVariable, Double]())
+      stepSensitivityMatrix.put(inVar, new mutable.HashMap[ScalarVariable, Double]())
+
+      val t = 0.0
+      val nominalIn = FMUHandler.getReal(instance, inVar)
+      val disturbance = computeDisturbance(inVar)
+      val outNominals = outVars.map(FMUHandler.getReal(instance, _)).toList
+
+      FMUHandler.doStep(instance, t, H)
+      val stepNominals = outVars.map(FMUHandler.getReal(instance, _)).toList
+
+      FMUHandler.terminate(instance)
+      instance = FMUHandler.instantiate(fmu, modDesc.getGuid)
+      FMUHandler.initialize(instance)
+
+      FMUHandler.setReal(instance, inVar, nominalIn + disturbance)
+      val instantDisturbances = outVars.map(FMUHandler.getReal(instance, _)).toList
+
+      FMUHandler.doStep(instance, t, H)
+      val stepDisturbances = outVars.map(FMUHandler.getReal(instance, _)).toList
+
+      FMUHandler.terminate(instance)
+      instance = FMUHandler.instantiate(fmu, modDesc.getGuid)
+      FMUHandler.initialize(instance)
+
+      for (i <- 0 until outVars.size){
+        val outVar = outVars(i)
+        val instantSensitivity = (instantDisturbances(i) - outNominals(i))/disturbance
+        val stepSensitivity = (stepDisturbances(i) - stepNominals(i))/disturbance
+
+        instantSensitivityMatrix(inVar).put(outVar, instantSensitivity)
+        stepSensitivityMatrix(inVar).put(outVar, stepSensitivity)
+      }
+
+    }
+
+    (instantSensitivityMatrix, stepSensitivityMatrix)
+  }
+
+  def printSensitivityResults(path: String,
+                              instantSensitivityMatrix: mutable.HashMap[ScalarVariable, mutable.HashMap[ScalarVariable, Double]],
+                              stepSensitivityMatrix: mutable.HashMap[ScalarVariable, mutable.HashMap[ScalarVariable, Double]],
+                              H: Double) = {
+    println(s"Sensitivity of ${path}:")
+    println("----------------")
+    for( (inVar, outVars) <- instantSensitivityMatrix){
+      for ((outVar, s) <- outVars) {
+        println(s"Instant Sensitivity: ${inVar.name} -> ${outVar.name} = $s")
+        println(s"Step (H=$H) Sensitivity: ${inVar.name} -> ${outVar.name} = ${stepSensitivityMatrix(inVar)(outVar)}")
+        if (outVar.outputDependencies.containsKey(inVar)){
+          println(s"${outVar.name} output dependencies: ${outVar.outputDependencies}")
+        }
+        if (isClose(s, 0.0, 1e-8) && outVar.outputDependencies.containsKey(inVar)){
+          println("Structure possibly inconsistent: no instant feedthrough.")
+        } else if (!isClose(s, 0.0, 1e-8) && !outVar.outputDependencies.containsKey(inVar)) {
+          println("Structure possibly inconsistent: instant impact where none is declared.")
+        }
+        println("----------------")
+      }
+    }
+  }
+
+
+}

BIN
src/test/resources/fmus/20-sim/threewatertank1.fmu


BIN
src/test/resources/fmus/20-sim/threewatertank2.fmu


BIN
src/test/resources/fmus/20-sim/threewatertankcontroller2.fmu


BIN
src/test/resources/fmus/dymola/MassSpringDamper1.fmu


BIN
src/test/resources/fmus/dymola/MassSpringDamper2.fmu


BIN
src/test/resources/fmus/omodelica/MassSpringDamper2.fmu


BIN
src/test/resources/fmus/omodelica/Scalar.fmu


Datei-Diff unterdrückt, da er zu groß ist
+ 95 - 0
src/test/resources/graphs/bound.graphml


Datei-Diff unterdrückt, da er zu groß ist
+ 95 - 0
src/test/resources/graphs/flat.graphml


Datei-Diff unterdrückt, da er zu groß ist
+ 149 - 0
src/test/resources/graphs/hierarchical.graphml


Datei-Diff unterdrückt, da er zu groß ist
+ 67 - 0
src/test/resources/graphs/splitted_graphs/1.graphml


Datei-Diff unterdrückt, da er zu groß ist
+ 54 - 0
src/test/resources/graphs/splitted_graphs/2.graphml


Datei-Diff unterdrückt, da er zu groß ist
+ 66 - 0
src/test/resources/graphs/splitted_graphs/3.graphml


Datei-Diff unterdrückt, da er zu groß ist
+ 54 - 0
src/test/resources/graphs/splitted_graphs/4.graphml


Datei-Diff unterdrückt, da er zu groß ist
+ 54 - 0
src/test/resources/graphs/sugar_all.graphml


Datei-Diff unterdrückt, da er zu groß ist
+ 95 - 0
src/test/resources/graphs/sugar_edges.graphml


Datei-Diff unterdrückt, da er zu groß ist
+ 54 - 0
src/test/resources/graphs/sugar_nodes.graphml


+ 10 - 0
src/test/resources/models/Scalar.mo

@@ -0,0 +1,10 @@
+model Scalar
+  input Real u;
+  Real x(start=1);
+  output Real yx;
+  output Real y;
+equation
+  der(x) = u;
+  yx = x;
+  y = u*u;
+end Scalar;

+ 241 - 0
src/test/scala/GraphMLTests.scala

@@ -0,0 +1,241 @@
+import java.io.File
+
+import modbat.mbt.MBT
+import org.scalatest.FlatSpec
+import scalax.collection.Graph
+
+import scala.xml.XML
+import scalax.collection.edge.LkDiEdge
+
+class GraphMLTests extends FlatSpec{
+  "XMLParser" should "Work" in {
+    val graphml_file = getClass.getResource("/graphs/flat.graphml")
+    val contents = XML.load(graphml_file)
+//    print(contents)
+  }
+
+  "Graph Lib" should "Allow for hierarchical graphs" in {
+    case class MyLabel(i: Int)
+
+    case class MyNode(l: String)
+
+    val outerEdge = LkDiEdge(MyNode("a"),MyNode("b"))(MyLabel(4))
+    outerEdge.label.i // all right: 4
+    val g = Graph(outerEdge)
+//    print(g)
+  }
+
+  "Transform flat graph" should "work" in {
+    val f = getClass.getResourceAsStream("/graphs/flat.graphml")
+    val graph = GraphMLLoader.createGraph(f)
+    assert(graph.nodes.size == 3)
+    assert(graph.edges.size == 3)
+    val instantiated = findNodeData(graph, "instantiated")
+
+    val edge = findEdge(graph, "e_terminate")
+    edge.source.value match {
+      case FlatNodeData(_, label) => assert(label=="instantiated")
+    }
+    edge.target.value match {
+      case FlatNodeData(_, label) => assert(label=="terminated")
+    }
+  }
+
+  def findEdge(graph: Graph[NodeData, LkDiEdge], lbl:String) = {
+    val res = graph.edges.find(n => n.label match {
+      case EdgeData(_,label,_) => label==lbl
+      case _ => false
+    })
+    assert(res.isDefined)
+    res.get
+  }
+
+  def findEdges(graph: Graph[NodeData, LkDiEdge], lbl:String) = {
+    val res = graph.edges.filter(n => n.label match {
+      case EdgeData(_,label,_) => label==lbl
+      case _ => false
+    })
+    assert(res.nonEmpty)
+    res
+  }
+
+  def findNodeData(graph: Graph[NodeData, LkDiEdge], lbl:String): NodeData = {
+    val res = graph.nodes.find(n => n.value match {
+      case FlatNodeData(_, label) => label==lbl
+      case HierarchicalNodeData(_, label, _) => label==lbl
+      case _ => false
+    })
+    assert(res.isDefined)
+    res.get
+  }
+
+  "Transform hierarchical graph" should "work" in {
+    val f = getClass.getResourceAsStream("/graphs/hierarchical.graphml")
+    val graph = GraphMLLoader.createGraph(f)
+    assert(graph.nodes.size == 3)
+    assert(graph.edges.size == 2)
+    val group1 = findNodeData(graph, "group1")
+    val subGraph = group1.asInstanceOf[HierarchicalNodeData].child
+
+    assert(subGraph.nodes.size == 2)
+    assert(subGraph.edges.size == 2)
+
+    val m2 = findEdge(subGraph, "m2")
+    assert(m2.source.value.asInstanceOf[FlatNodeData].label == "something")
+    assert(m2.target.value.asInstanceOf[FlatNodeData].label == "instantiated")
+  }
+
+  "Syntactic sugar for edges" should "expand edges" in {
+    val f = getClass.getResourceAsStream("/graphs/sugar_edges.graphml")
+    val graph = GraphMLLoader.createGraph(f)
+    assert(graph.nodes.size == 3)
+    assert(graph.edges.size == 4)
+
+    val eTerminate = findEdge(graph, "e_terminate")
+    val eInterrupt = findEdge(graph, "e_Interrupt")
+    val eInstantiated = findEdge(graph, "e_instantiated")
+
+    assert(eTerminate.source.value.asInstanceOf[FlatNodeData].label == "instantiated")
+    assert(eTerminate.target.value.asInstanceOf[FlatNodeData].label == "terminated")
+    assert(eInterrupt.source.value.asInstanceOf[FlatNodeData].label == "instantiated")
+    assert(eInterrupt.target.value.asInstanceOf[FlatNodeData].label == "terminated")
+    assert(eInstantiated.source.value.asInstanceOf[FlatNodeData].label == "instantiated")
+    assert(eInstantiated.target.value.asInstanceOf[FlatNodeData].label == "instantiated")
+  }
+
+  def assertEdgeUpperBound(graph: Graph[NodeData, LkDiEdge], edge: String, n : Int) = {
+    assert(findEdge(graph, edge).label match {
+      case EdgeData(_,_,bound) => bound == n
+      case _ => false
+    })
+  }
+
+  def assertEdgeNoUpperBound(graph: Graph[NodeData, LkDiEdge], edge: String) = assertEdgeUpperBound(graph, edge, Int.MaxValue)
+
+  "Syntactic sugar for edges" should "extract upper bound if any" in {
+    val f = getClass.getResourceAsStream("/graphs/bound.graphml")
+    val graph = GraphMLLoader.createGraph(f)
+    assert(graph.nodes.size == 3)
+    assert(graph.edges.size == 3)
+
+    assertEdgeNoUpperBound(graph, "e_terminate")
+    assertEdgeUpperBound(graph, "e_instantiated", 1)
+  }
+
+  "Syntactic sugar for nodes" should "expand nodes" in {
+    val f = getClass.getResourceAsStream("/graphs/sugar_nodes.graphml")
+    val graph = GraphMLLoader.createGraph(f)
+    assert(graph.nodes.size == 3)
+    assert(graph.edges.size == 2)
+
+    findNodeData(graph, "instantiated")
+    findNodeData(graph, "something")
+    findNodeData(graph, "terminated")
+
+    val terminateEdges = findEdges(graph, "e_Interrupt")
+
+    terminateEdges.foreach(e => {
+      assert(e.target.value.asInstanceOf[FlatNodeData].label == "terminated")
+    })
+    assert(terminateEdges.map(_.source.value.asInstanceOf[FlatNodeData].label).toSet == Set("instantiated", "something"))
+  }
+
+  "Syntactic sugar" should "expand nodes and edges" in {
+    val f = getClass.getResourceAsStream("/graphs/sugar_all.graphml")
+    val graph = GraphMLLoader.createGraph(f)
+    assert(graph.nodes.size == 4)
+    assert(graph.edges.size == 8)
+
+    val a = findNodeData(graph, "aState")
+    val b = findNodeData(graph, "bState")
+    val c = findNodeData(graph, "cState")
+    val d = findNodeData(graph, "dState")
+
+    val e1 = findEdges(graph, "e_someEvent")
+    val e2 = findEdges(graph, "e_someOtherEvent")
+
+    assert(e1.map(_.source.value.asInstanceOf[FlatNodeData].label).toSet == Set("aState", "bState"))
+    assert(e1.map(_.target.value.asInstanceOf[FlatNodeData].label).toSet == Set("cState", "dState"))
+
+    assert(e2.map(_.source.value.asInstanceOf[FlatNodeData].label).toSet == Set("aState", "bState"))
+    assert(e2.map(_.target.value.asInstanceOf[FlatNodeData].label).toSet == Set("cState", "dState"))
+  }
+
+  "Multiple GraphMLs" should "be merge into one graph, regardless of order" in {
+    def graph_conditions(graphList: List[String]) = {
+      val graph = GraphMLLoader.createGraphFromPath(graphList).get
+
+      assert(graph.nodes.size == 5)
+      assert(graph.edges.size == 9)
+
+      findNodeData(graph, "start")
+      findNodeData(graph, "instantiated")
+      findNodeData(graph, "running")
+      findNodeData(graph, "terminated")
+      findNodeData(graph, "freed")
+
+      val eInit = findEdge(graph, "init")
+      val eInstantiated = findEdge(graph, "e_instantiated")
+      val eStart = findEdge(graph, "e_start")
+      val eGo = findEdge(graph, "e_go")
+      val eRestart = findEdge(graph, "e_restart")
+      val eCancel = findEdge(graph, "e_cancel")
+      val eFree = findEdge(graph, "e_free")
+      val eInterrupt = findEdges(graph, "e_Interrupt")
+
+      assert(eInit.source.value.asInstanceOf[FlatNodeData].label == "start")
+      assert(eInit.target.value.asInstanceOf[FlatNodeData].label == "instantiated")
+
+      assert(eInstantiated.source.value.asInstanceOf[FlatNodeData].label == "instantiated")
+      assert(eInstantiated.target.value.asInstanceOf[FlatNodeData].label == "instantiated")
+
+      assert(eStart.source.value.asInstanceOf[FlatNodeData].label == "instantiated")
+      assert(eStart.target.value.asInstanceOf[FlatNodeData].label == "running")
+      assert(eGo.source.value.asInstanceOf[FlatNodeData].label == "instantiated")
+      assert(eGo.target.value.asInstanceOf[FlatNodeData].label == "running")
+      assert(eRestart.source.value.asInstanceOf[FlatNodeData].label == "running")
+      assert(eRestart.target.value.asInstanceOf[FlatNodeData].label == "instantiated")
+      assert(eCancel.source.value.asInstanceOf[FlatNodeData].label == "running")
+      assert(eCancel.target.value.asInstanceOf[FlatNodeData].label == "instantiated")
+
+      assert(eFree.source.value.asInstanceOf[FlatNodeData].label == "terminated")
+      assert(eFree.target.value.asInstanceOf[FlatNodeData].label == "freed")
+
+      assert(eInterrupt.map(_.source.value.asInstanceOf[FlatNodeData].label).toSet == Set("instantiated", "running"))
+      eInterrupt.foreach(_.target.value.asInstanceOf[FlatNodeData].label == "terminated")
+    }
+
+    val graphList = List("/graphs/splitted_graphs/1.graphml",
+      "/graphs/splitted_graphs/2.graphml",
+      "/graphs/splitted_graphs/3.graphml",
+      "/graphs/splitted_graphs/4.graphml")
+
+    graphList.permutations.foreach(permutation => graph_conditions(permutation))
+  }
+
+  "FMIGraphModel" should "have the correct initial state" in {
+    val m = new FMIGraphModel()
+    val mbt = MBT.launch(m)
+    assert(mbt.currentState.toString() == "start")
+  }
+
+  "FMIGraphModel" should "load the correct fmi states" in {
+    val graph = GraphMLLoader.createGraphFromPath(
+                    List("/graphs/fmi/fmi.graphml",
+                      "/graphs/fmi/free.graphml",
+                      "/graphs/fmi/reset.graphml")).get
+    findNodeData(graph, "start")
+    findNodeData(graph, "instantiated")
+    findNodeData(graph, "terminated")
+    findNodeData(graph, "freed")
+  }
+
+
+  "FMIGraphModel" should "work in MBT" in {
+    val p = getClass.getResource("/fmus/20-sim/threewatertank1.fmu").getPath
+    val f = new File(p)
+    val conf = Config(nruns = 10, loopLimit = 5)
+    MBTRunner.run_mbt(f, conf, classOf[FMIGraphModel])
+  }
+
+}

+ 186 - 0
src/test/scala/MiscTests.scala

@@ -0,0 +1,186 @@
+import java.io.File
+
+import breeze.linalg._
+import breeze.plot._
+import org.apache.commons.lang3.SystemUtils
+import org.intocps.fmi.{Fmi2Status, IFmuCallback}
+import org.intocps.fmi.jnifmuapi.Factory
+import org.intocps.orchestration.coe.modeldefinition.ModelDescription
+import org.scalatest.FlatSpec
+import org.slf4j.{Logger, LoggerFactory}
+
+import scala.collection.JavaConverters._
+
+class MiscTests extends FlatSpec {
+
+  "Factory" should "load without any issues" in {
+    Factory.checkApi()
+  }
+
+  "FMU" should "load without any issues" in {
+    val path = getClass.getResource("/fmus/20-sim/threewatertank1.fmu").getPath
+    val (fmu, modDesc) = FMUHandler.load(path)
+    val instance = FMUHandler.instantiate(fmu, modDesc.getGuid)
+
+    FMUHandler.initialize(instance)
+    FMUHandler.terminate(instance)
+    FMUHandler.cleanup(fmu)
+  }
+
+  "FMU" should "run at least one cosim step" in {
+    assume(SystemUtils.IS_OS_WINDOWS)
+    val path = getClass.getResource("/fmus/omodelica/Scalar.fmu").getPath
+    val (fmu, modDesc) = FMUHandler.load(path)
+    val instance = FMUHandler.instantiate(fmu, modDesc.getGuid)
+
+    val vars = modDesc.getScalarVariables.asScala
+
+    val u = vars.find(_.name == "u").get
+    val x = vars.find(_.name == "x").get
+
+    FMUHandler.initialize(instance)
+
+    FMUHandler.setReal(instance, u, -1.0)
+
+    val H = 0.1
+
+    instance.doStep(0.0, H, true)
+
+    val xVal = FMUHandler.getReal(instance, x)
+
+    assert(isClose(xVal, 1.0-H, 1e-10))
+
+    FMUHandler.terminate(instance)
+    FMUHandler.cleanup(fmu)
+  }
+
+  "Scalar Test" should "guess the input approximation of omodelica scalar fmu" in {
+    assume(SystemUtils.IS_OS_WINDOWS)
+    val path = getClass.getResource("/fmus/omodelica/Scalar.fmu").getPath
+
+    val (fmu, modDesc) = FMUHandler.load(path)
+    val instance = FMUHandler.instantiate(fmu, modDesc.getGuid)
+
+    val model = new ScalarFMUModel()
+
+    val (order, error) = InputApproximationAnalyzer.findInputApproxOrder(fmu, modDesc, model)
+
+    println(order, error)
+
+//    val f = Figure()
+//    val p = f.subplot(0)
+    val (ts, _, _) = InputApproximationAnalyzer.runScalarInApproxExperiment(order, modDesc, instance, model, None /*Some(p)*/)
+//    p.legend = true
+//    f.saveas("plot.pdf")
+
+    assert(order == 0)
+    assert(isClose(error, 0.0, 1e-3))
+
+    FMUHandler.terminate(instance)
+    FMUHandler.cleanup(fmu)
+  }
+
+  "Dynamic structure inference" should "work correctly" in {
+    assume(SystemUtils.IS_OS_WINDOWS)
+    val path = getClass.getResource("/fmus/omodelica/Scalar.fmu").getPath
+    val H = 0.01
+
+    val (fmu, modDesc) = FMUHandler.load(path)
+
+    val (instantSensitivityMatrix, stepSensitivityMatrix) = SensitivityAnalyzer.computeSensitivity(fmu, modDesc, H, false)
+
+    SensitivityAnalyzer.printSensitivityResults(path, instantSensitivityMatrix, stepSensitivityMatrix, H)
+
+    for( (inVar, outVars) <- instantSensitivityMatrix) {
+      for ((outVar, s) <- outVars) {
+        if (inVar.name=="u" && outVar.name == "y"){
+          assert(isClose(s, 1.0))
+        }
+        if (inVar.name=="u" && outVar.name == "x"){
+          assert(isClose(stepSensitivityMatrix(inVar)(outVar), H))
+        }
+      }
+    }
+    FMUHandler.cleanup(fmu)
+  }
+
+  "MBT" should "work in FMIModel" in {
+    assume(SystemUtils.IS_OS_WINDOWS)
+    val p = getClass.getResource("/fmus/omodelica/Scalar.fmu").getPath
+    val f = new File(p)
+    val conf = Config(nruns = 10)
+    MBTRunner.run_mbt(f, conf, classOf[FMIModel])
+  }
+
+  "MBT" should "draw FMIModel" in {
+    MBTRunner.draw_fmi_model()
+  }
+
+  "Dynamic structure inference" should "work in directory" in {
+    val path = getClass.getResource("/fmus/20-sim/threewatertank1.fmu").getPath
+    val H = 0.01
+
+    val dirFile = new File(path)
+    assert(dirFile.exists())
+
+    FMUHandler.applyCmdFMUsDir(dirFile, (f) => SensitivityAnalyzer.computeSensitivity(f, H, false))
+
+  }
+
+  ignore should "work" in {
+    val f = Figure()
+    val p = f.subplot(0)
+    val x = linspace(0.0, 1.0)
+    p += plot(x, x * 2.0)
+    p += plot(x, x * 3.0, '.')
+    p.xlabel = "x axis"
+    p.ylabel = "y axis"
+//    f.saveas("lines.pdf")
+  }
+
+  "Lagrange polynomials" should "be correct" in {
+    def u(t:Double) = {
+      t*t
+    }
+
+    val uApprox = (th:Double) => InputApproximationAnalyzer.lagrangePolynomials(1.0, 2, 1.0, u)(th)
+    val t = linspace(-10.0,10.0, 100)
+
+    t.foreach(t => assert(isClose(u(t), uApprox(t), 1e-5)))
+
+//    val real = t.map(u)
+//    val approx = t.map(uApprox)
+//    val f = Figure()
+//    val p = f.subplot(0)
+//    p += plot(t, real)
+//    p += plot(t, approx, '.')
+//    f.saveas("plot.pdf")
+  }
+
+  "Repeated init and terminate" should "not crash fmi lib" in {
+    val logger: Logger = LoggerFactory.getLogger("RepeatedTest")
+
+    Factory.checkApi()
+    val path = getClass.getResource("/fmus/20-sim/threewatertank1.fmu").getPath
+    val fmuFile = new File(path)
+    val fmu = Factory.create(fmuFile)
+    fmu.load()
+    val modDesc = new ModelDescription(fmu.getModelDescription())
+
+    for (i <- 0 until 10){
+      logger.info(s"Attempt: $i")
+      // If null is passed instead of FMUCallBack, then the fmi lib may crash.
+      val instance = fmu.instantiate(modDesc.getGuid(), "fmu", true, false, new IFmuCallback() {
+        override def log(instanceName: String, status: Fmi2Status, category: String, message: String): Unit = {}
+        override def stepFinished(fmi2Status: Fmi2Status): Unit = {}
+      })
+      instance.setupExperiment(false, 0.0, 0.0, true, 1.0)
+      instance.enterInitializationMode()
+      instance.exitInitializationMode()
+      instance.terminate()
+      instance.freeInstance()
+    }
+    fmu.unLoad()
+  }
+
+}