Repository: flowcean/flowcean
Pull request #265: Add xor example
Author: login: ChristianWieck
Labels: name: example
description: Examples showing the usage of the framework
name: tools
Created: 2025-04-09T11:25:09Z | Merged: 2025-04-20T15:40:25Z

This PR:
- Adds a simple `xor` example. This example demonstrates how a learner can be trained and used with the prediction loop and the `DataFrameAdapter`.

Based on #264


Diff:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 03e73bd64..3e97e46f1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -79,7 +79,7 @@ jobs:
     name: Examples
     strategy:
       matrix:
-        example: ["boiler", "failure_time_prediction", "linear_data", "one_tank", "energy_system"]
+        example: ["boiler", "failure_time_prediction", "linear_data", "one_tank", "energy_system", "xor"]
     runs-on:
       - self-hosted
       - itl
diff --git a/examples/xor/data.csv b/examples/xor/data.csv
new file mode 100644
index 000000000..732748255
--- /dev/null
+++ b/examples/xor/data.csv
@@ -0,0 +1,5 @@
+x,y,z
+0,0,0
+0,1,1
+1,0,1
+0,0,0
diff --git a/examples/xor/learn.py b/examples/xor/learn.py
new file mode 100644
index 000000000..fd53230d3
--- /dev/null
+++ b/examples/xor/learn.py
@@ -0,0 +1,32 @@
+from pathlib import Path
+
+from flowcean.core.strategies import learn_offline
+from flowcean.polars.environments.dataframe import DataFrame
+from flowcean.sklearn.regression_tree import RegressionTree
+
+
+def main() -> None:
+    # Load the data from a CSV file
+    data = DataFrame.from_csv("data.csv")
+
+    # Create a regression tree model and train it on the data
+    learner = RegressionTree()
+    inputs = [
+        "x",
+        "y",
+    ]
+    outputs = ["z"]
+
+    model = learn_offline(
+        data,
+        learner,
+        inputs,
+        outputs,
+    )
+
+    # Save the trained model to a file
+    model.save(Path("xor_model.fml"))
+
+
+if __name__ == "__main__":
+    main()
diff --git a/examples/xor/predict.py b/examples/xor/predict.py
new file mode 100644
index 000000000..349c3d2d8
--- /dev/null
+++ b/examples/xor/predict.py
@@ -0,0 +1,29 @@
+from pathlib import Path
+
+from flowcean.core.model import Model
+from flowcean.core.tool.predict import start_prediction_loop
+from flowcean.polars.adapter.dataframe_adapter import DataFrameAdapter
+from flowcean.polars.environments.dataframe import DataFrame
+
+
+def main() -> None:
+    # Load the trained model
+    model = Model.load(Path("xor_model.fml"))
+
+    # Create a fake adapter from a DataFrame
+    adapter = DataFrameAdapter(
+        DataFrame.from_csv("data.csv"),
+        input_features=["x", "y"],
+        result_path="result.csv",
+    )
+
+    # Run the prediction loop. The loop is blocking until the Adapter signals
+    # a stop when nor more data is available or the program is interrupted.
+    start_prediction_loop(
+        model,
+        adapter,
+    )
+
+
+if __name__ == "__main__":
+    main()
diff --git a/justfile b/justfile
index c311e93de..143eb7ade 100644
--- a/justfile
+++ b/justfile
@@ -31,7 +31,7 @@ docs-serve:
   @echo "🚀 Serving documentation: Running mkdocs"
   @uv run mkdocs serve
 
-examples: examples-alp examples-boiler examples-coffee_machine examples-failure_time_prediction examples-linear_data examples-one_tank examples-robot_localization_failure examples-energy_system
+examples: examples-alp examples-boiler examples-coffee_machine examples-failure_time_prediction examples-linear_data examples-one_tank examples-robot_localization_failure examples-energy_system examples-xor
 
 examples-alp:
   @echo "🚀 Running example: Automatic Lashing Platform"
@@ -64,3 +64,8 @@ examples-robot_localization_failure:
 examples-energy_system:
   echo "🚀 Running example: Energy System"
   @uv run --directory ./examples/energy_system/ run.py
+
+examples-xor:
+  @echo "🚀 Running example: XOR"
+  @uv run --directory ./examples/xor/ learn.py
+  @uv run --directory ./examples/xor/ predict.py
diff --git a/src/flowcean/polars/adapter/__init__.py b/src/flowcean/polars/adapter/__init__.py
index e69de29bb..b3717da48 100644
--- a/src/flowcean/polars/adapter/__init__.py
+++ b/src/flowcean/polars/adapter/__init__.py
@@ -0,0 +1,3 @@
+__all__ = ["DataFrameAdapter"]
+
+from .dataframe_adapter import DataFrameAdapter
diff --git a/tests/polars/adapter/test_dataframe_adapter.py b/tests/polars/adapter/test_dataframe_adapter.py
new file mode 100644
index 000000000..af939c708
--- /dev/null
+++ b/tests/polars/adapter/test_dataframe_adapter.py
@@ -0,0 +1,65 @@
+import tempfile
+import unittest
+
+import polars as pl
+from polars.testing import assert_frame_equal
+
+from flowcean.core.adapter import Stop
+from flowcean.polars import DataFrame
+from flowcean.polars.adapter import DataFrameAdapter
+
+
+class TestDataFrameAdapter(unittest.TestCase):
+    def test_adapter(self) -> None:
+        data = pl.DataFrame(
+            {
+                "A": [1, 2, 3],
+                "B": [4, 5, 6],
+            },
+        )
+        send_data = pl.DataFrame(
+            {
+                "data": [1, 2, 3],
+            },
+        )
+
+        pulled_data = pl.DataFrame()
+        with tempfile.NamedTemporaryFile() as f:
+            adapter = DataFrameAdapter(
+                DataFrame(data),
+                input_features=["A", "B"],
+                result_path=f.name,
+            )
+
+            # Start the adapter
+            adapter.start()
+
+            try:
+                while True:
+                    pulled_data = pl.concat(
+                        [pulled_data, adapter.get_data().collect()],
+                        how="vertical",
+                    )
+            except Stop:
+                pass
+
+            # Send some data back to the adapter
+            for row in send_data.iter_rows(named=True):
+                adapter.send_data(pl.DataFrame(row).lazy())
+
+            # Stop the adapter
+            adapter.stop()
+
+            assert_frame_equal(
+                pulled_data,
+                data,
+            )
+
+            assert_frame_equal(
+                adapter.result_df,
+                send_data,
+            )
+
+
+if __name__ == "__main__":
+    unittest.main()


Reviews:
user: login: newLabAspect
body: Should we add this example in the justfile and the examples in the pipeline?
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-15T09:43:03Z
user: login: ChristianWieck
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T09:52:36Z
user: login: ChristianWieck
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T11:02:26Z
user: login: newLabAspect
state: APPROVED
author_association: MEMBER
submitted_at: 2025-04-20T15:38:57Z

Review comments:
diff_hunk: @@ -0,0 +1,54 @@
+from collections.abc import Iterable
+from typing import cast
+
+import polars as pl
+
+from flowcean.core.adapter import Adapter
+from flowcean.core.adapter.adapter import Stop
+from flowcean.polars.environments.dataframe import DataFrame
+
+
+class DataFrameAdapter(Adapter):
path: src/flowcean/polars/adapter/dataframe_adapter.py
user: login: newLabAspect
body: Do we need tests for this class?
created_at: 2025-04-15T09:42:38Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,54 @@
+from collections.abc import Iterable
+from typing import cast
+
+import polars as pl
+
+from flowcean.core.adapter import Adapter
+from flowcean.core.adapter.adapter import Stop
+from flowcean.polars.environments.dataframe import DataFrame
+
+
+class DataFrameAdapter(Adapter):
path: src/flowcean/polars/adapter/dataframe_adapter.py
user: login: ChristianWieck
body: Would be better. I'll have a look how easily a test can be written.
created_at: 2025-04-15T09:52:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,54 @@
+from collections.abc import Iterable
+from typing import cast
+
+import polars as pl
+
+from flowcean.core.adapter import Adapter
+from flowcean.core.adapter.adapter import Stop
+from flowcean.polars.environments.dataframe import DataFrame
+
+
+class DataFrameAdapter(Adapter):
path: src/flowcean/polars/adapter/dataframe_adapter.py
user: login: ChristianWieck
body: I added a simple test for the `DataFrameAdapter`.
created_at: 2025-04-15T11:02:26Z
author_association: CONTRIBUTOR

PR comments:
user: login: ChristianWieck
created_at: 2025-04-15T09:51:59Z
body: > Should we add this example in the justfile and the examples in the pipeline?

I'm not sure how the pipeline likes to store a model and its outputs. And most/all functionality should already be covered by the unit tests, so we would not really gain much. But I'm open to it.
author_association: CONTRIBUTOR
user: login: newLabAspect
created_at: 2025-04-15T13:49:48Z
body: An advantage of having the example in the pipeline would be to make sure that we do not brake the example through changes in the functionalities. This is probably a good thing if we assume future changes in the adapter and tool structures.
author_association: MEMBER
user: login: ChristianWieck
created_at: 2025-04-16T05:51:20Z
body: > An advantage of having the example in the pipeline would be to make sure that we do not brake the example through changes in the functionalities. This is probably a good thing if we assume future changes in the adapter and tool structures.

Alright. I added the example to the just file and the ci/cd tasks.
author_association: CONTRIBUTOR