Repository: precice-forschungsprojekt/config-checker
Pull request #102: Mapping data processing adjustments
Author: login: orlandoisepic
Created: 2025-04-10T20:24:02Z | Merged: 2025-04-15T05:29:06Z

Mapping Rule got a lot smarter:
For every mapping:
- Instead of just checking whether a mapping has a corresponding read OR write data, it now gets checked that both exist!
  - This will also be mentioned in the helpful message

This now recognizes a lot more faulty mappings, as can be seen from the adjusted test config.

Regarding the code: 
- The checks for missing data processing violation can be summarized into one, per mapping type (JIT or regular) and direction.
- The check for len(...) > 0 is not necessary anymore, as len(...)=0 already includes the check for correct mesh

TODO:
- [x] Differentiate output to identify which data processing element is missing


Diff:
diff --git a/preciceconfigchecker/rules/mapping.py b/preciceconfigchecker/rules/mapping.py
index 19d7514..32c44ab 100644
--- a/preciceconfigchecker/rules/mapping.py
+++ b/preciceconfigchecker/rules/mapping.py
@@ -1,14 +1,24 @@
 from networkx.classes import Graph
+from enum import Enum
 from precice_config_graph.nodes import ParticipantNode, MeshNode, MappingNode, Direction, MappingConstraint, \
     MappingMethod, CouplingSchemeNode, MultiCouplingSchemeNode, CouplingSchemeType, ExchangeNode, M2NNode, \
-    WriteDataNode, \
-    ReadDataNode
+    WriteDataNode, ReadDataNode, ActionNode, ExportNode, WatchPointNode, WatchIntegralNode
 from preciceconfigchecker.rule_utils import rule_error_message
 from preciceconfigchecker.rule import Rule
 from preciceconfigchecker.severity import Severity
 from preciceconfigchecker.violation import Violation
 
 
+class MissingDataProcessing(Enum):
+    """
+        Define which data processing elements are missing.
+        Data processing elements are write-data, read-data and similar elements that can write or read data.
+    """
+    WRITE_DATA = "write-data"
+    READ_DATA = "read-data"
+    READ_DATA_AND_WRITE_DATA = "read-and-write"
+
+
 class MappingRule(Rule):
     name = "Mapping rules."
 
@@ -30,7 +40,7 @@ def __init__(self, participant: ParticipantNode, mesh: MeshNode, direction: Dire
         def format_explanation(self) -> str:
             out: str = (f"The participant {self.participant.name} is specifying a {self.direction.value}-mapping "
                         f"{self.connecting_word} mesh {self.mesh.name}, which is his own mesh.")
-            out += f"The mapping is from participant {self.participant.name} to participant {self.participant.name}."
+            out += f"\nThe mapping is on {self.participant.name}'s own meshes, which is forbidden."
             return out
 
         def format_possible_solutions(self) -> list[str]:
@@ -141,8 +151,9 @@ def format_explanation(self) -> str:
 
         def format_possible_solutions(self) -> list[str]:
             out: list[str] = []
-            out += [f"Create a coupling-scheme between participants {self.parent.name} and {self.stranger.name} with an"
-                    f"exchange to exchange data between them."]
+            out += [
+                f"Create a coupling-scheme between participants {self.parent.name} and {self.stranger.name} with an "
+                f"exchange to exchange data between them."]
             out += ["Otherwise, please remove the mapping to improve readability."]
             return out
 
@@ -287,7 +298,7 @@ class MappingMissingDataProcessingViolation(Violation):
         severity = Severity.ERROR
 
         def __init__(self, parent: ParticipantNode, stranger: ParticipantNode, mesh_parent: MeshNode,
-                     mesh_stranger: MeshNode, direction: Direction):
+                     mesh_stranger: MeshNode, direction: Direction, missing_data_processing: MissingDataProcessing):
             self.parent = parent
             self.stranger = stranger
             self.mesh_parent = mesh_parent
@@ -296,20 +307,59 @@ def __init__(self, parent: ParticipantNode, stranger: ParticipantNode, mesh_pare
             if self.direction == Direction.READ:
                 self.connecting_word = "from"
                 self.inverse_connector = "to"
+                self.inverse_direction = Direction.WRITE
             elif self.direction == Direction.WRITE:
                 self.connecting_word = "to"
                 self.inverse_connector = "from"
+                self.inverse_direction = Direction.READ
+            self.missing_data_processing = missing_data_processing
 
         def format_explanation(self) -> str:
-            return (f"Participant {self.parent.name} is specifying a {self.direction.value}-mapping "
-                    f"{self.connecting_word} participant {self.stranger.name}'s mesh {self.mesh_stranger.name} "
-                    f"{self.inverse_connector} participant {self.parent.name}'s mesh {self.mesh_parent.name}, but does "
-                    f"not define a corresponding {self.direction.value}-data element on mesh {self.mesh_parent.name}.")
+            out: str = (f"Participant {self.parent.name} is specifying a {self.direction.value}-mapping "
+                        f"{self.connecting_word} participant {self.stranger.name}'s mesh {self.mesh_stranger.name} "
+                        f"{self.inverse_connector} participant {self.parent.name}'s mesh {self.mesh_parent.name}.")
+            if self.missing_data_processing == MissingDataProcessing.READ_DATA:
+                if self.direction == Direction.WRITE:
+                    out += (f"\nHowever, it seems like {self.stranger.name} does not {self.inverse_direction.value} "
+                            f"{self.inverse_connector} {self.mesh_stranger.name}.")
+                elif self.direction == Direction.READ:
+                    out += (f"\nHowever, it seems like {self.parent.name} does not {self.direction.value} "
+                            f"{self.connecting_word} {self.mesh_parent.name}. ")
+            elif self.missing_data_processing == MissingDataProcessing.WRITE_DATA:
+                if self.direction == Direction.WRITE:
+                    out += (f"\nHowever, it seems like {self.parent.name} does not {self.direction.value} "
+                            f"{self.connecting_word} {self.mesh_parent.name}.")
+                elif self.direction == Direction.READ:
+                    out += (f"\nHowever, it seems like {self.stranger.name} does not {self.inverse_direction.value} "
+                            f"{self.inverse_connector} {self.mesh_stranger.name}.")
+            elif self.missing_data_processing == MissingDataProcessing.READ_DATA_AND_WRITE_DATA:
+                out += (f"\n However, it seems like {self.parent.name} does not {self.direction.value} "
+                        f"{self.connecting_word} {self.mesh_parent.name} and {self.stranger.name} does not "
+                        f"{self.inverse_direction.value} {self.inverse_connector} {self.mesh_stranger.name}.")
+            return out
 
         def format_possible_solutions(self) -> list[str]:
-            return [f"Please add a {self.direction.value}-data element to participant {self.parent.name}, which uses "
-                    f"mesh {self.mesh_parent.name} by participant {self.parent.name}.",
-                    "Otherwise, please remove the mapping to improve readability."]
+            out: list[str] = []
+            if self.missing_data_processing == MissingDataProcessing.READ_DATA:
+                if self.direction == Direction.WRITE:
+                    out = [f"Please ensure that {self.stranger.name} {self.inverse_direction.value}s "
+                           f"{self.inverse_connector} {self.mesh_stranger.name}."]
+                elif self.direction == Direction.READ:
+                    out = [f"Please ensure that {self.parent.name} {self.direction.value}s {self.connecting_word} "
+                           f"{self.mesh_parent.name}."]
+            elif self.missing_data_processing == MissingDataProcessing.WRITE_DATA:
+                if self.direction == Direction.WRITE:
+                    out = [f"Please ensure that {self.parent.name} {self.direction.value}s {self.connecting_word} "
+                           f"{self.mesh_parent.name}."]
+                elif self.direction == Direction.READ:
+                    out = [f"Please ensure that {self.stranger.name} {self.inverse_direction.value}s "
+                           f"{self.inverse_connector} {self.mesh_stranger.name}."]
+            elif self.missing_data_processing == MissingDataProcessing.READ_DATA_AND_WRITE_DATA:
+                out = [f"Please ensure that {self.parent.name} {self.direction.value}s {self.connecting_word} "
+                       f"{self.mesh_parent.name} and {self.stranger.name} {self.inverse_direction.value}s "
+                       f"{self.inverse_connector} {self.mesh_stranger.name}.",
+                       "Otherwise, please remove the mapping to improve readability."]
+            return out
 
     class JustInTimeMappingApiAccessViolation(Violation):
         """
@@ -535,25 +585,67 @@ class JustInTimeMappingMissingDataProcessingViolation(Violation):
         """
         severity = Severity.ERROR
 
-        def __init__(self, parent: ParticipantNode, stranger: ParticipantNode, mesh: MeshNode, direction: Direction):
+        def __init__(self, parent: ParticipantNode, stranger: ParticipantNode, mesh: MeshNode, direction: Direction,
+                     missing_data_processing: MissingDataProcessing):
             self.parent = parent
             self.stranger = stranger
             self.mesh = mesh
             self.direction = direction
             if self.direction == Direction.READ:
                 self.connecting_word = "from"
+                self.inverse_connector = "to"
+                self.inverse_direction = Direction.WRITE
             elif self.direction == Direction.WRITE:
                 self.connecting_word = "to"
+                self.inverse_connector = "from"
+                self.inverse_direction = Direction.READ
+            self.missing_data_processing = missing_data_processing
 
         def format_explanation(self) -> str:
-            return (f"Participant {self.parent.name} is specifying a just-in-time {self.direction.value}-mapping "
-                    f"{self.connecting_word} participant {self.stranger.name}'s mesh {self.mesh.name}, but does not "
-                    f"define a corresponding {self.direction.value}-data element on mesh {self.mesh.name}.")
+            out: str = (f"Participant {self.parent.name} is specifying a just-in-time {self.direction.value}-mapping "
+                        f"{self.connecting_word} participant {self.stranger.name}'s mesh {self.mesh.name}. ")
+            if self.missing_data_processing == MissingDataProcessing.READ_DATA:
+                if self.direction == Direction.WRITE:
+                    out += (f"\nHowever, it seems like {self.stranger.name} does not {self.inverse_direction.value} "
+                            f"{self.inverse_connector} {self.mesh.name}.")
+                elif self.direction == Direction.READ:
+                    out += (f"\nHowever, it seems like {self.parent.name} does not {self.direction.value} "
+                            f"{self.connecting_word} {self.mesh.name}.")
+                elif self.missing_data_processing == MissingDataProcessing.WRITE_DATA:
+                    if self.direction == Direction.WRITE:
+                        out += (f"\nHowever, it seems like {self.parent.name} does not {self.inverse_direction.value} "
+                                f"{self.inverse_connector} {self.mesh.name}.")
+                elif self.direction == Direction.READ:
+                    out += (f"\nHowever, it seems like {self.stranger.name} does not {self.direction.value} "
+                            f"{self.connecting_word} {self.mesh.name}.")
+            elif self.missing_data_processing == MissingDataProcessing.READ_DATA_AND_WRITE_DATA:
+                out += (f"\nHowever, it seems like {self.parent.name} does not {self.direction.value} "
+                        f"{self.connecting_word} {self.mesh.name} and {self.stranger.name} does not "
+                        f"{self.inverse_direction.value} {self.inverse_connector} {self.mesh.name}.")
+            return out
 
         def format_possible_solutions(self) -> list[str]:
-            return [f"Please add a {self.direction.value}-data element to participant {self.parent.name}, which uses "
-                    f"mesh {self.mesh.name} by participant {self.stranger.name}.",
-                    "Otherwise, please remove the mapping to improve readability."]
+            out: list[str] = []
+            if self.missing_data_processing == MissingDataProcessing.READ_DATA:
+                if self.direction == Direction.WRITE:
+                    out = [f"Please ensure that {self.stranger.name} {self.inverse_direction.value}s "
+                           f"{self.inverse_connector} {self.mesh.name}."]
+                elif self.direction == Direction.READ:
+                    out = [f"Please ensure that {self.parent.name} {self.direction.value}s "
+                           f"{self.connecting_word} {self.mesh.name}."]
+            elif self.missing_data_processing == MissingDataProcessing.WRITE_DATA:
+                if self.direction == Direction.WRITE:
+                    out = [f"Please ensure that {self.parent.name} {self.inverse_direction.value}s "
+                           f"{self.inverse_connector} {self.mesh.name}."]
+                elif self.direction == Direction.READ:
+                    out = [f"Please ensure that {self.stranger.name} {self.direction.value}s "
+                           f"{self.connecting_word} {self.mesh.name}."]
+            elif self.missing_data_processing == MissingDataProcessing.READ_DATA_AND_WRITE_DATA:
+                out = [f"Please ensure that {self.parent.name} {self.direction.value}s {self.connecting_word} "
+                       f"{self.mesh.name} and {self.stranger.name} {self.inverse_direction.value}s "
+                       f"{self.inverse_connector} {self.mesh.name}.",
+                       "Otherwise, please remove the mapping to improve readability."]
+            return out
 
     def check(self, graph: Graph) -> list[Violation]:
         violations: list[Violation] = []
@@ -682,35 +774,35 @@ def check(self, graph: Graph) -> list[Violation]:
                             violations.append(
                                 self.JustInTimeMappingApiAccessViolation(participant_parent, participant_stranger,
                                                                          mesh_stranger, direction))
+                write_datas = []
+                read_datas = []
+                # Check if there are corresponding read-/write-data elements for the mapping
                 if direction == Direction.WRITE:
-                    write_datas: list[WriteDataNode] = participant_parent.write_data
-                    if len(write_datas) == 0:
-                        violations.append(
-                            self.JustInTimeMappingMissingDataProcessingViolation(participant_parent,
-                                                                                 participant_stranger,
-                                                                                 mesh_stranger, direction))
-                    if len(write_datas) > 0 and not any(
-                            data_processing_belongs_to_mapping(write_data, mesh_stranger) for write_data in
-                            write_datas):
-                        # Data gets mapped but not written
-                        violations.append(
-                            self.JustInTimeMappingMissingDataProcessingViolation(participant_parent,
-                                                                                 participant_stranger,
-                                                                                 mesh_stranger, direction))
+                    write_datas = get_write_datas_of_mesh(mesh_stranger, participant_parent)
+                    read_datas = get_read_datas_of_mesh(mesh_stranger, participant_stranger)
                 elif direction == Direction.READ:
-                    read_datas: list[ReadDataNode] = participant_parent.read_data
-                    if len(read_datas) == 0:
-                        violations.append(
-                            self.JustInTimeMappingMissingDataProcessingViolation(participant_parent,
-                                                                                 participant_stranger,
-                                                                                 mesh_stranger, direction))
-                    if len(read_datas) > 0 and not any(
-                            data_processing_belongs_to_mapping(read_data, mesh_stranger) for read_data in read_datas):
-                        # Data gets mapped, but not read
-                        violations.append(
-                            self.JustInTimeMappingMissingDataProcessingViolation(participant_parent,
-                                                                                 participant_stranger,
-                                                                                 mesh_stranger, direction))
+                    read_datas = get_read_datas_of_mesh(mesh_stranger, participant_parent)
+                    write_datas = get_write_datas_of_mesh(mesh_stranger, participant_stranger)
+                    # In a JIT write-mapping, Parent should write to Stranger's mesh; Stranger should read from their own mesh
+                    # In a JIT read-mapping, Parent should read from Stranger's mesh; Stranger should write to their own mesh
+                if len(write_datas) == 0 and len(read_datas) == 0:
+                    violations.append(
+                        self.JustInTimeMappingMissingDataProcessingViolation(participant_parent,
+                                                                             participant_stranger,
+                                                                             mesh_stranger, direction,
+                                                                             MissingDataProcessing.READ_DATA_AND_WRITE_DATA))
+                elif len(write_datas) != 0 and len(read_datas) == 0:
+                    violations.append(
+                        self.JustInTimeMappingMissingDataProcessingViolation(participant_parent,
+                                                                             participant_stranger,
+                                                                             mesh_stranger, direction,
+                                                                             MissingDataProcessing.READ_DATA))
+                elif len(write_datas) == 0 and len(read_datas) != 0:
+                    violations.append(
+                        self.JustInTimeMappingMissingDataProcessingViolation(participant_parent,
+                                                                             participant_stranger,
+                                                                             mesh_stranger, direction,
+                                                                             MissingDataProcessing.WRITE_DATA))
             # Only regular mappings here
             else:
                 if direction == Direction.READ:
@@ -725,35 +817,33 @@ def check(self, graph: Graph) -> list[Violation]:
                         violations.append(
                             self.MappingDirectionViolation(participant_parent, participant_stranger, mesh_parent,
                                                            mesh_stranger, direction))
-
-                # Check if there is a corresponding read-/write-data element for the mapping
+                write_datas = []
+                read_datas = []
+                # Check if there are corresponding read-/write-data elements for the mapping
                 if direction == Direction.WRITE:
-                    write_datas: list[WriteDataNode] = participant_parent.write_data
-                    if len(write_datas) == 0:
-                        # In a write-mapping, Parent has to write the data
-                        violations.append(
-                            self.MappingMissingDataProcessingViolation(participant_parent, participant_stranger,
-                                                                       mesh_parent, mesh_stranger, direction))
-                    # In a write-mapping, Parent should write to their own mesh
-                    if len(write_datas) > 0 and not any(
-                            data_processing_belongs_to_mapping(write_data, mesh_parent) for write_data in write_datas):
-                        violations.append(
-                            self.MappingMissingDataProcessingViolation(participant_parent, participant_stranger,
-                                                                       mesh_parent, mesh_stranger, direction))
+                    # In a write-mapping, Parent should write to their own mesh; Stranger should read from their own mesh
+                    write_datas = get_write_datas_of_mesh(mesh_parent, participant_parent)
+                    read_datas = get_read_datas_of_mesh(mesh_stranger, participant_stranger)
                 elif direction == Direction.READ:
-                    read_datas: list[ReadDataNode] = participant_parent.read_data
-                    if len(read_datas) == 0:
-                        # In a read mapping, Parent should read data
-                        violations.append(
-                            self.MappingMissingDataProcessingViolation(participant_parent, participant_stranger,
-                                                                       mesh_parent, mesh_stranger, direction))
-                    # In a read-mapping, Parent should read from their own mesh
-                    if len(read_datas) > 0 and not any(
-                            data_processing_belongs_to_mapping(read_data, mesh_parent) for read_data in read_datas):
-                        violations.append(
-                            self.MappingMissingDataProcessingViolation(participant_parent, participant_stranger,
-                                                                       mesh_parent, mesh_stranger, direction))
-
+                    # In a read-mapping, Parent should read from their own mesh; Stranger should write to their own
+                    write_datas = get_write_datas_of_mesh(mesh_stranger, participant_stranger)
+                    read_datas = get_read_datas_of_mesh(mesh_parent, participant_parent)
+                if len(read_datas) == 0 and len(write_datas) == 0:
+                    # only read is missing
+                    violations.append(
+                        self.MappingMissingDataProcessingViolation(participant_parent, participant_stranger,
+                                                                   mesh_parent, mesh_stranger, direction,
+                                                                   MissingDataProcessing.READ_DATA_AND_WRITE_DATA))
+                elif len(read_datas) == 0 and len(write_datas) != 0:
+                    violations.append(
+                        self.MappingMissingDataProcessingViolation(participant_parent, participant_stranger,
+                                                                   mesh_parent, mesh_stranger, direction,
+                                                                   MissingDataProcessing.READ_DATA))
+                elif len(read_datas) != 0 and len(write_datas) == 0:
+                    violations.append(
+                        self.MappingMissingDataProcessingViolation(participant_parent, participant_stranger,
+                                                                   mesh_parent, mesh_stranger, direction,
+                                                                   MissingDataProcessing.WRITE_DATA))
             # Both JIT and regular mappings share these violations
             # Check for correct m2n between mapping participants
             m2n = get_m2n_of_participants(m2ns, participant_parent, participant_stranger)
@@ -828,19 +918,6 @@ def get_m2n_of_participants(m2ns: list[M2NNode], participant_a: ParticipantNode,
     return None
 
 
-def data_processing_belongs_to_mapping(data_processing: WriteDataNode | ReadDataNode, mesh: MeshNode) -> bool:
-    """
-        This function evaluates whether the given data-processing node is valid for a mapping,
-        defined by the given mesh.
-        It is assumed that the data_processing node is specified by the parent of the mapping,
-        and that the mesh is the mesh that would be expected in a correct data_processing.
-        :param data_processing: The data-processing node, either a write-data node or a read-data node.
-        :param mesh: The mesh that would be expected in a correct data_processing.
-        :return: True, if the data-processing node is valid for the mapping, False otherwise.
-    """
-    return mesh == data_processing.mesh
-
-
 def exchange_belongs_to_mapping(exchange: ExchangeNode, direction: Direction, participant_stranger: ParticipantNode,
                                 mesh_stranger: MeshNode) -> bool:
     """
@@ -982,3 +1059,49 @@ def filter_m2n_nodes(graph: Graph) -> list[M2NNode]:
         if isinstance(node, M2NNode):
             m2ns.append(node)
     return m2ns
+
+
+def get_write_datas_of_mesh(mesh: MeshNode, parent: ParticipantNode) -> list[WriteDataNode | ActionNode]:
+    """
+        Get all writers of the given mesh, which gets provided by parent.
+        Data can be written by a write-data node or an action node.
+        :param mesh: The mesh to get writers for.
+        :param parent: The participant providing mesh.
+        :return: A list of all writers of the given mesh, if any.
+    """
+    writers: list[WriteDataNode | ActionNode] = []
+    for action in parent.actions:
+        if action.mesh == mesh:
+            writers.append(action)
+    for write_data in parent.write_data:
+        if write_data.mesh == mesh:
+            writers.append(write_data)
+    return writers
+
+
+def get_read_datas_of_mesh(mesh: MeshNode, parent: ParticipantNode) -> list[
+    ReadDataNode | ExportNode | ActionNode | WatchPointNode | WatchIntegralNode]:
+    """
+        Get all readers of the given mesh, which gets provided by parent.
+        Data can be read by a read-data node, an export node, an action node, or a watch-point /-integral node.
+        :param mesh: The mesh to get writers for.
+        :param parent: The participant providing mesh.
+        :return: A list of all readers of the given mesh, if any.
+    """
+    readers: list[ReadDataNode | ExportNode | ActionNode | WatchPointNode | WatchIntegralNode] = []
+    # Export “reads” all meshes
+    for export in parent.exports:
+        readers.append(export)
+    for action in parent.actions:
+        if action.mesh == mesh:
+            readers.append(action)
+    for read_data in parent.read_data:
+        if read_data.mesh == mesh:
+            readers.append(read_data)
+    for watchpoint in parent.watchpoints:
+        if watchpoint.mesh == mesh:
+            readers.append(watchpoint)
+    for watch_integral in parent.watch_integrals:
+        if watch_integral.mesh == mesh:
+            readers.append(watch_integral)
+    return readers
diff --git a/tests/coupling-scheme-mapping/coupling-scheme-mapping_test.py b/tests/coupling-scheme-mapping/coupling-scheme-mapping_test.py
index 5ef0829..e4ddb4c 100644
--- a/tests/coupling-scheme-mapping/coupling-scheme-mapping_test.py
+++ b/tests/coupling-scheme-mapping/coupling-scheme-mapping_test.py
@@ -1,7 +1,8 @@
 from preciceconfigchecker.rules.disjoint_simulations import DisjointSimulationsRule as d
 from preciceconfigchecker.rules.coupling_scheme_mapping import CouplingSchemeMappingRule as c
+from preciceconfigchecker.rules.mapping import MappingRule as m, MissingDataProcessing as mdp
 from tests.test_utils import assert_equal_violations, create_graph, get_actual_violations
-from precice_config_graph.nodes import ParticipantNode, MeshNode, DataNode
+from precice_config_graph.nodes import ParticipantNode, MeshNode, DataNode, Direction
 
 
 def test_coupling_scheme_mapping():
@@ -31,13 +32,21 @@ def test_coupling_scheme_mapping():
                 m_propagator = node
             elif node.name == "Alligator-Mesh":
                 m_alligator = node
+            elif node.name == "Instigator-Mesh":
+                m_instigator = node
 
     violations_expected = [
+
         c.MissingMappingCouplingSchemeViolation(p_generator, p_propagator, m_generator),
+
         c.MissingMappingCouplingSchemeViolation(p_generator, p_propagator, m_propagator),
+
         c.MissingMappingAPIAccessCouplingSchemeViolation(p_alligator, p_instigator, m_alligator),
+
         d.SharedDataDisjointSimulationsViolation(d_color, frozenset([frozenset([p_generator, p_propagator, p_elevator]),
-                                                                     frozenset([p_alligator, p_instigator])]))
+                                                                     frozenset([p_alligator, p_instigator])])),
+        m.JustInTimeMappingMissingDataProcessingViolation(p_alligator, p_instigator, m_instigator, Direction.WRITE,
+                                                          mdp.READ_DATA)
     ]
 
     assert_equal_violations("Coupling-scheme-mapping-test", violations_expected, violations_actual)
diff --git a/tests/mapping/mapping_test.py b/tests/mapping/mapping_test.py
index 5dbad8e..88b675b 100644
--- a/tests/mapping/mapping_test.py
+++ b/tests/mapping/mapping_test.py
@@ -1,6 +1,6 @@
 from precice_config_graph.nodes import ParticipantNode, MeshNode, Direction, MappingConstraint, MappingMethod, DataNode
 
-from preciceconfigchecker.rules.mapping import MappingRule as m
+from preciceconfigchecker.rules.mapping import MappingRule as m, MissingDataProcessing as mdp
 from preciceconfigchecker.rules.m2n_exchange import M2NExchangeRule as mn
 from preciceconfigchecker.rules.data_use_read_write import DataUseReadWriteRule as d
 from preciceconfigchecker.rules.coupling_scheme_mapping import CouplingSchemeMappingRule as csm
@@ -19,12 +19,12 @@ def test_mapping():
             if node.name == "Color":
                 d_color = node
         elif isinstance(node, ParticipantNode):
-            if node.name == "Alligator":
-                p_alligator = node
-            elif node.name == "Generator":
+            if node.name == "Generator":
                 p_generator = node
             elif node.name == "Propagator":
                 p_propagator = node
+            elif node.name == "Alligator":
+                p_alligator = node
             elif node.name == "Instigator":
                 p_instigator = node
             elif node.name == "Elevator":
@@ -32,12 +32,12 @@ def test_mapping():
             elif node.name == "Incinerator":
                 p_incinerator = node
         elif isinstance(node, MeshNode):
-            if node.name == "Alligator-Mesh":
-                m_alligator = node
-            elif node.name == "Generator-Mesh":
+            if node.name == "Generator-Mesh":
                 m_generator = node
             elif node.name == "Propagator-Mesh":
                 m_propagator = node
+            elif node.name == "Alligator-Mesh":
+                m_alligator = node
             elif node.name == "Instigator-Mesh":
                 m_instigator = node
             elif node.name == "Elevator-Mesh":
@@ -75,19 +75,21 @@ def test_mapping():
 
         m.MissingM2NMappingViolation(p_incinerator, p_propagator, m_propagator, Direction.READ),
 
+        m.MappingMissingDataProcessingViolation(p_propagator, p_generator, m_propagator, m_generator, Direction.WRITE,
+                                                mdp.READ_DATA),
         mn.MissingM2NEchangeViolation(p_incinerator),
 
-        m.MappingMissingDataProcessingViolation(p_propagator, p_generator, m_propagator, m_generator, Direction.WRITE),
-
         d.DataNotExchangedViolation(d_color, p_generator, p_alligator),
 
+        d.DataNotExchangedViolation(d_color, p_propagator, p_incinerator),
+
         d.DataNotExchangedViolation(d_color, p_instigator, p_elevator),
 
-        csm.MissingMappingCouplingSchemeViolation(p_generator, p_propagator, m_generator),
+        csm.MissingMappingCouplingSchemeViolation(p_propagator, p_generator, m_propagator),
 
         csm.MissingMappingCouplingSchemeViolation(p_incinerator, p_propagator, m_incinerator),
 
-        csm.MissingMappingCouplingSchemeViolation(p_generator, p_propagator, m_propagator)
+        csm.MissingMappingCouplingSchemeViolation(p_generator, p_propagator, m_generator)
     ]
 
     assert_equal_violations("Mapping-test", violations_expected, violations_actual)
diff --git a/tests/mapping/precice-config.xml b/tests/mapping/precice-config.xml
index d8f77b4..9c77fea 100644
--- a/tests/mapping/precice-config.xml
+++ b/tests/mapping/precice-config.xml
@@ -52,7 +52,8 @@
                 to="Propagator-Mesh"
                 from="Generator-Mesh"
                 constraint="consistent"/>
-        <read-data name="Color" mesh="Propagator-Mesh"/>
+        <read-data name="Color" mesh="Propagator-Mesh" />
+        <write-data name="Color" mesh="Propagator-Mesh" />
     </participant>
 
     <participant name="Alligator">
@@ -64,6 +65,8 @@
                 from="Generator-Mesh"
                 constraint="consistent"/>
         <read-data name="Color" mesh="Generator-Mesh"/>
+        <write-data name="Color" mesh="Alligator-Mesh" />
+        <read-data name="Color" mesh="Alligator-Mesh" />
     </participant>
 
     <participant name="Instigator">
@@ -81,6 +84,7 @@
             constraint="consistent"/>
         <read-data name="Color" mesh="Alligator-Mesh"/>
         <write-data name="Color" mesh="Alligator-Mesh"/>
+        <write-data name="Color" mesh="Instigator-Mesh" />
     </participant>
 
     <participant name="Elevator">
@@ -160,6 +164,6 @@
         <time-window-size value="0.01"/>
         <max-time value="0.3"/>
         <exchange data="Color" mesh="Generator-Mesh" from="Generator" to="Propagator"/>
-        <exchange data="Color" mesh="Propagator-Mesh" to="Propagator" from="Generator"/>
+        <exchange data="Color" mesh="Propagator-Mesh" from="Propagator" to="Generator"/>
     </coupling-scheme:serial-explicit>
 </precice-configuration>


Reviews:
user: login: s3n-w6i
body: Thanks, looks good. Just miss the types :/
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T15:57:36Z
user: login: TheFirstGameruler
body: This import please!!!
Otherwise everything looks good.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T16:19:06Z
user: login: s3n-w6i
body: The imports could indeed use some love. Looks very good otherwise 👌
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-14T16:21:52Z
user: login: orlandoisepic
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T05:28:05Z

Review comments:
diff_hunk: @@ -287,7 +298,7 @@ class MappingMissingDataProcessingViolation(Violation):
         severity = Severity.ERROR
 
         def __init__(self, parent: ParticipantNode, stranger: ParticipantNode, mesh_parent: MeshNode,
-                     mesh_stranger: MeshNode, direction: Direction):
+                     mesh_stranger: MeshNode, direction: Direction, missing_data_processing):
path: preciceconfigchecker/rules/mapping.py
user: login: s3n-w6i
body: What's the type?
created_at: 2025-04-14T15:55:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -535,25 +585,67 @@ class JustInTimeMappingMissingDataProcessingViolation(Violation):
         """
         severity = Severity.ERROR
 
-        def __init__(self, parent: ParticipantNode, stranger: ParticipantNode, mesh: MeshNode, direction: Direction):
+        def __init__(self, parent: ParticipantNode, stranger: ParticipantNode, mesh: MeshNode, direction: Direction,
+                     missing_data_processing):
path: preciceconfigchecker/rules/mapping.py
user: login: s3n-w6i
body: Type :)
created_at: 2025-04-14T15:55:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,8 +1,9 @@
 from networkx.classes import Graph
+from enum import Enum
 from precice_config_graph.nodes import ParticipantNode, MeshNode, MappingNode, Direction, MappingConstraint, \
     MappingMethod, CouplingSchemeNode, MultiCouplingSchemeNode, CouplingSchemeType, ExchangeNode, M2NNode, \
     WriteDataNode, \
-    ReadDataNode
+    ReadDataNode, ActionNode, ExportNode, WatchPointNode, WatchIntegralNode
path: preciceconfigchecker/rules/mapping.py
user: login: TheFirstGameruler
body: We've done this before, right?
Isn't it better:
`from precise_config_graph import nodes as n`
and then use it as follows:
`n.XYZ`
created_at: 2025-04-14T16:04:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -287,7 +298,7 @@ class MappingMissingDataProcessingViolation(Violation):
         severity = Severity.ERROR
 
         def __init__(self, parent: ParticipantNode, stranger: ParticipantNode, mesh_parent: MeshNode,
-                     mesh_stranger: MeshNode, direction: Direction):
+                     mesh_stranger: MeshNode, direction: Direction, missing_data_processing):
path: preciceconfigchecker/rules/mapping.py
user: login: TheFirstGameruler
body: At the beginning of the class...
InnerClass: MissingDataPrecessing(Enum)
Should be stated.
created_at: 2025-04-14T16:10:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,8 +1,9 @@
 from networkx.classes import Graph
+from enum import Enum
 from precice_config_graph.nodes import ParticipantNode, MeshNode, MappingNode, Direction, MappingConstraint, \
     MappingMethod, CouplingSchemeNode, MultiCouplingSchemeNode, CouplingSchemeType, ExchangeNode, M2NNode, \
     WriteDataNode, \
-    ReadDataNode
+    ReadDataNode, ActionNode, ExportNode, WatchPointNode, WatchIntegralNode
path: preciceconfigchecker/rules/mapping.py
user: login: orlandoisepic
body: I said it before, right?
Lets do that in a separate PR, for _all_ rules
created_at: 2025-04-15T05:28:05Z
author_association: CONTRIBUTOR

PR comments:
user: login: orlandoisepic
created_at: 2025-04-10T20:34:06Z
body: @s3n-w6i can you please check the output for a MappingMissingDataProcessingViolation ? 
I find it slightly confusing as the wording is
> Participant Propagator is specifying a write-mapping to participant Generator's mesh Generator-Mesh from participant Propagator's mesh Propagator-Mesh. 
     However, it seems like participant Propagator does not write to mesh Propagator-Mesh or participant Generator does not read from mesh Generator-Mesh.
     ==> Please make sure that Propagator writes to Propagator-Mesh and Generator reads from Generator-Mesh.

This means it is not directly obvious WHICH of the two is missing. 
One possible way of fixing this would be to create different violations for all three cases (one is missing, the other is missing or both are), but this would of course blow up the code even more, as this would mean 4 more violations in total (2 for regular, 2 for JIT) and I guess that you wouldn't like that :)
The other way I see is with additional parameters for the existing violations, and some `if-else` blocks in the format. This would also blow up the code though.

If you have any other suggestions, please let me know
author_association: CONTRIBUTOR
user: login: s3n-w6i
created_at: 2025-04-11T14:03:31Z
body: Do we need seperate violations, so this is not something that can be solved with formatting?
author_association: CONTRIBUTOR
user: login: orlandoisepic
created_at: 2025-04-11T14:04:36Z
body: We could solve it with boolean parameters to indicate which is correct and if-else in the existing violations
author_association: CONTRIBUTOR