├── conftest.py ├── src ├── __init__.py ├── model_locator.py ├── classifiers │ └── causalitydetection │ │ ├── classificationmodel.py │ │ └── causalclassifier.py ├── util │ ├── constants.py │ └── loader.py ├── converters │ ├── sentencetolabels │ │ ├── labelrecovery.py │ │ ├── labeler.py │ │ └── model.py │ ├── graphtotestsuite │ │ └── testsuiteconverter.py │ └── labelstograph │ │ ├── graphconverter.py │ │ └── eventconnector.py ├── cira.py ├── data │ └── test.py └── api │ └── service.py ├── doc ├── visualization-graph.PNG └── visualization-labels.PNG ├── docker-compose.yaml ├── .vscode └── settings.json ├── .devcontainer └── devcontainer.json ├── Dockerfile ├── pytest.ini ├── download-models.sh ├── .github └── workflows │ ├── pytest.yml │ ├── docker-image-app.yml │ └── docker-image-dev.yml ├── Dockerfile.dev ├── test ├── classifiers │ └── causalitidetection │ │ └── test_causalclassifier.py ├── data │ ├── graph │ │ ├── IntermediateNode │ │ │ ├── test_node_get_precedence_value.py │ │ │ ├── test_get_parents_ordered_by_precedence.py │ │ │ └── test_node_condense.py │ │ ├── Edge │ │ │ └── test_edge_repr.py │ │ ├── Node │ │ │ ├── test_get_root.py │ │ │ └── test_add_delete_incoming.py │ │ ├── Graph │ │ │ ├── test_get_node.py │ │ │ ├── test_graph_repr.py │ │ │ └── test_graph_eq.py │ │ ├── test_permute_configurations.py │ │ ├── test_graph_serialization.py │ │ └── test_graph_deserialization.py │ ├── test │ │ ├── test_suite_serialization.py │ │ └── test_suite_deserialization.py │ └── labels │ │ ├── test_labels.py │ │ ├── test_get_label_by_type_and_position.py │ │ ├── test_labels_serialization.py │ │ └── test_labels_deserialization.py ├── converters │ ├── sentencetolabels │ │ ├── labeler │ │ │ └── test_comma_resolver.py │ │ ├── labelrecovery │ │ │ └── test_label_exceptive_clause.py │ │ ├── labelingconverter │ │ │ ├── test_merge_labels.py │ │ │ ├── test_junctors_between.py │ │ │ ├── test_connect_labels.py │ │ │ └── test_token_labeling.py │ │ └── test_labeler.py │ ├── graphtotestsuite │ │ ├── test_testsuiteconverter.py │ │ └── testsuiteconverter │ │ │ ├── test_map_node_to_parameter.py │ │ │ └── test_get_testcase_configuration.py │ └── labelstograph │ │ ├── test_graphconverter.py │ │ ├── eventresolver │ │ ├── test_join_event_labels.py │ │ ├── test_neighbor_detector.py │ │ ├── test_get_attribute_of_eventlabel_group.py │ │ ├── test_get_events_in_order.py │ │ └── test_eventresolver.py │ │ ├── eventconnector │ │ ├── test_junctor_map.py │ │ ├── test_generate_initial_nodenet.py │ │ └── test_eventconnector.py │ │ └── graphconverter │ │ └── test_resolve_exceptive_negations.py ├── test_model_locator.py ├── api │ └── service │ │ ├── test_recover_labels.py │ │ ├── test_api_integrate.py │ │ └── test_api_isolated.py └── app │ ├── test_app_isolated.py │ └── test_app_integrated.py ├── setup.py ├── .gitignore ├── static └── sentences │ ├── sentence-1d.json │ ├── sentence-1.json │ ├── sentence-16.json │ ├── sentence-1b.json │ ├── sentence-1c.json │ ├── sentence-18.json │ ├── sentence-8.json │ ├── sentence-2.json │ ├── sentence-3.json │ ├── sentence-10.json │ └── sentence-12.json └── app.py /conftest.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- 1 | from . import converters -------------------------------------------------------------------------------- /doc/visualization-graph.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianFrattini/cira/HEAD/doc/visualization-graph.PNG -------------------------------------------------------------------------------- /doc/visualization-labels.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JulianFrattini/cira/HEAD/doc/visualization-labels.PNG -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | services: 4 | cira: 5 | container_name: ghcr.io/julianfrattini/cira 6 | build: . 7 | ports: 8 | - "8080:8000" 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.testing.pytestArgs": [ 3 | "test" 4 | ], 5 | "python.testing.unittestEnabled": false, 6 | "python.testing.pytestEnabled": true 7 | } -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: 2 | { 3 | "name": "Prebuild Image", 4 | "image": "ghcr.io/julianfrattini/cira-dev:latest", 5 | "customizations": { 6 | "vscode": { 7 | "extensions": [ 8 | "ms-python.python" 9 | ] 10 | } 11 | }, 12 | "containerEnv": { 13 | "DEV_CONTAINER": "TRUE" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ghcr.io/julianfrattini/cira-dev:latest 2 | 3 | WORKDIR /cira 4 | 5 | # Install Python dependencies and setup cira version 6 | COPY setup.py . 7 | COPY README.md . 8 | RUN pip3 install -e . 9 | 10 | # Set DEV_CONTAINER to true such that the code references the models in the container 11 | ENV DEV_CONTAINER=TRUE 12 | 13 | COPY ./src ./src 14 | COPY ./app.py ./app.py 15 | 16 | CMD ["python", "app.py"] -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | # pytest.ini 2 | [pytest] 3 | addopts = 4 | --cov-report term-missing 5 | --cov=src 6 | -s 7 | -vv 8 | testpaths = 9 | test 10 | markers = 11 | unit: unit level tests cases 12 | system: system level test cases 13 | syslabeler: system level test cases for the labeler 14 | integration: integration level tests 15 | staging: tests that are currently under development 16 | env = 17 | MODEL_CONTAINER_DEV = container/cira-labeler.ckpt 18 | -------------------------------------------------------------------------------- /download-models.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | FILE_MODELS='model/cira-models.zip' 4 | FILE_CLASSIFIER='model/cira-classifier.bin' 5 | FILE_LABELER='model/cira-labeler.ckpt' 6 | 7 | if [ -e $FILE_CLASSIFIER ] && [ -e $FILE_LABELER ] ; then 8 | echo "$FILE_MODELS already exists" 9 | else 10 | echo "Download $FILE_MODELS" 11 | curl -# "https://zenodo.org/record/7186287/files/cira-models.zip?download=1" -o $FILE_MODELS --create-dirs 12 | echo "Extract $FILE_MODELS" 13 | unzip -o $FILE_MODELS -d model 14 | fi 15 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yml: -------------------------------------------------------------------------------- 1 | name: Pytest 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | test: 15 | 16 | runs-on: ubuntu-latest 17 | container: ghcr.io/julianfrattini/cira-dev:latest 18 | 19 | steps: 20 | - uses: actions/checkout@v3 21 | 22 | - name: Install dependencies 23 | run: | 24 | pip install -e ".[dev]" 25 | 26 | - name: Test with pytest 27 | run: | 28 | pytest 29 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | FROM python:3.10-bullseye 2 | 3 | # Install Rust compiler 4 | RUN curl https://sh.rustup.rs -sSf | sh -s -- -y 5 | ENV PATH="/root/.cargo/bin:${PATH}" 6 | 7 | WORKDIR /app 8 | 9 | COPY ./model/cira-classifier.bin model/cira-classifier.bin 10 | COPY ./model/cira-labeler.ckpt model/cira-labeler.ckpt 11 | 12 | ENV MODEL_CLASSIFICATION_DEV=/app/model/cira-classifier.bin 13 | ENV MODEL_LABELING_DEV=/app/model/cira-labeler.ckpt 14 | 15 | # Required for Jupyter 16 | RUN pip3 install ipykernel 17 | 18 | # Install Python dependencies and setup cira version 19 | COPY setup.py . 20 | COPY README.md . 21 | RUN pip3 install --no-cache-dir -e ".[dev]" 22 | -------------------------------------------------------------------------------- /src/model_locator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import dotenv 3 | 4 | 5 | def __load_model_env(model: str) -> str: 6 | path_from_env_file = os.getenv(model, '') 7 | file_exist = os.path.isfile(path_from_env_file) 8 | if file_exist: 9 | return path_from_env_file 10 | 11 | path_inside_container = os.getenv(model + '_DEV', '') 12 | file_exist = os.path.isfile(path_inside_container) 13 | if file_exist: 14 | return path_inside_container 15 | 16 | raise NameError(f'Unable to locate model from env {model}') 17 | 18 | 19 | dotenv.load_dotenv() 20 | LABELING = __load_model_env('MODEL_LABELING') 21 | CLASSIFICATION = __load_model_env('MODEL_CLASSIFICATION') 22 | -------------------------------------------------------------------------------- /test/classifiers/causalitidetection/test_causalclassifier.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src import model_locator 4 | from src.classifiers.causalitydetection.causalclassifier import CausalClassifier 5 | 6 | @pytest.fixture(scope="module") 7 | def sut() -> CausalClassifier: 8 | return CausalClassifier(model_locator.CLASSIFICATION) 9 | 10 | @pytest.mark.system 11 | @pytest.mark.parametrize('sentence, causal', [ 12 | ('If the red button is pushed the system shuts down.', True), 13 | ('The architecture of the system utilizes a broker pattern.', False) 14 | ]) 15 | def test_classifier(sut: CausalClassifier, sentence, causal): 16 | classification, confidence = sut.classify(sentence=sentence) 17 | assert classification == causal -------------------------------------------------------------------------------- /test/data/graph/IntermediateNode/test_node_get_precedence_value.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.graph import IntermediateNode 4 | 5 | @pytest.mark.unit 6 | def test_disjunction(): 7 | node: IntermediateNode = IntermediateNode(id='i', conjunction=False, precedence=False) 8 | assert node.get_precedence_value() == 1 9 | 10 | @pytest.mark.unit 11 | def test_conjunction(): 12 | node: IntermediateNode = IntermediateNode(id='i', conjunction=True, precedence=False) 13 | assert node.get_precedence_value() == 2 14 | 15 | @pytest.mark.unit 16 | def test_disjunction_overruled_precedence(): 17 | node: IntermediateNode = IntermediateNode(id='i', conjunction=False, precedence=True) 18 | assert node.get_precedence_value() == 3 -------------------------------------------------------------------------------- /test/data/graph/Edge/test_edge_repr.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.graph import Edge, EventNode 4 | 5 | @pytest.mark.unit 6 | def test_repr(): 7 | n1 = EventNode(id='n1', labels=None, variable='node 1', condition='is present') 8 | n2 = EventNode(id='n2', labels=None, variable='node 2', condition='is active') 9 | e = Edge(origin=n1, target=n2) 10 | 11 | assert e.__repr__() == 'n1 ---> n2' 12 | 13 | @pytest.mark.unit 14 | def test_repr_negated(): 15 | n1 = EventNode(id='n1', labels=None, variable='node 1', condition='is present') 16 | n2 = EventNode(id='n2', labels=None, variable='node 2', condition='is active') 17 | e = Edge(origin=n1, target=n2, negated=True) 18 | 19 | assert e.__repr__() == 'n1 -~-> n2' -------------------------------------------------------------------------------- /test/data/graph/Node/test_get_root.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | import pytest 3 | 4 | from src.data.graph import Node, Edge 5 | 6 | @pytest.fixture 7 | def tree() -> Tuple[Node, Node]: 8 | # generate two generic nodes 9 | root = Node(id='r') 10 | leaf = Node(id='l') 11 | 12 | # connect the root and the leaf with an edge 13 | edge = Edge(origin=leaf, target=root, negated=False) 14 | root.incoming.append(edge) 15 | leaf.outgoing.append(edge) 16 | 17 | return leaf, root 18 | 19 | @pytest.mark.unit 20 | def test_get_root_of_leaf(tree): 21 | leaf, root = tree 22 | 23 | assert leaf.get_root() == root 24 | 25 | @pytest.mark.unit 26 | def test_get_root_of_root(tree): 27 | _, root = tree 28 | 29 | assert root.get_root() == root -------------------------------------------------------------------------------- /src/classifiers/causalitydetection/classificationmodel.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from transformers import BertModel 3 | 4 | 5 | class CausalClassificationModel(torch.nn.Module): 6 | def __init__(self, n_classes: int, pre_trained_model_name: str='bert-base-cased'): 7 | super(CausalClassificationModel, self).__init__() 8 | self.bert = BertModel.from_pretrained(pre_trained_model_name) 9 | self.drop = torch.nn.Dropout(p=0.3) 10 | self.out = torch.nn.Linear(self.bert.config.hidden_size, n_classes) 11 | 12 | def forward(self, input_ids, attention_mask): 13 | _, pooled_output = self.bert(input_ids=input_ids, attention_mask=attention_mask, return_dict=False) 14 | output = self.drop(pooled_output) 15 | return self.out(output) 16 | 17 | -------------------------------------------------------------------------------- /test/data/graph/Graph/test_get_node.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.graph import Graph, Node 4 | 5 | @pytest.mark.unit 6 | def test_existing(): 7 | nodes = [Node(id=f'n{i}') for i in range(5)] 8 | graph = Graph(nodes=nodes, root=None, edges=None) 9 | 10 | assert graph.get_node(id='n0') == nodes[0] 11 | 12 | @pytest.mark.unit 13 | def test_nonexisting_returns_none(): 14 | nodes = [Node(id=f'n{i}') for i in range(5)] 15 | graph = Graph(nodes=nodes, root=None, edges=None) 16 | 17 | assert graph.get_node(id='n10') == None 18 | 19 | @pytest.mark.unit 20 | def test_nonexisting_output(capsys): 21 | nodes = [Node(id=f'n{i}') for i in range(5)] 22 | graph = Graph(nodes=nodes, root=None, edges=None) 23 | 24 | graph.get_node(id='n10') 25 | captured = capsys.readouterr() 26 | assert f'No node with id n10 found in {nodes}' in captured.out -------------------------------------------------------------------------------- /test/data/graph/Graph/test_graph_repr.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.graph import Graph, IntermediateNode, EventNode, Edge 4 | 5 | @pytest.mark.integration 6 | def test_graph_representation(): 7 | root = IntermediateNode(id='i1', conjunction=True) 8 | cause1 = EventNode(id='c1', variable='an error', condition='occurs') 9 | cause2 = EventNode(id='c2', variable='the admin', condition='is notified') 10 | root.add_incoming(cause1) 11 | root.add_incoming(cause2, negated=True) 12 | 13 | effect = EventNode(id='e1', variable='the admin', condition='receives a push notification') 14 | effect.add_incoming(root) 15 | 16 | graph = Graph(nodes = [cause1, cause2, root, effect], root=root, edges=None) 17 | 18 | assert graph.__repr__() == f'([{cause1.variable}].({cause1.condition}) && NOT [{cause2.variable}].({cause2.condition})) ===> [{effect.variable}].({effect.condition})' -------------------------------------------------------------------------------- /test/data/test/test_suite_serialization.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from dataclasses import asdict 4 | from src.data.test import Suite, Parameter 5 | 6 | @pytest.mark.unit 7 | def test_test(): 8 | cause = Parameter(id='c', variable='cause', condition='occurs') 9 | effect = Parameter(id='e', variable='effect', condition='happens') 10 | cases=[ 11 | {'c': False, 'e': False}, 12 | {'c': True, 'e': True} 13 | ] 14 | suite = Suite(conditions=[cause], expected=[effect], cases=cases) 15 | serialized = asdict(suite) 16 | 17 | expected = { 18 | 'conditions': [{'id': 'c', 'variable': 'cause', 'condition': 'occurs'}], 19 | 'expected': [{'id': 'e', 'variable': 'effect', 'condition': 'happens'}], 20 | 'cases': [ 21 | {'c': False, 'e': False}, 22 | {'c': True, 'e': True} 23 | ] 24 | } 25 | assert serialized == expected -------------------------------------------------------------------------------- /test/data/test/test_suite_deserialization.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.test import Suite, Parameter, from_dict 4 | 5 | @pytest.mark.unit 6 | def test_deserialization(): 7 | suite_dict = { 8 | 'conditions': [{'id': 'c', 'variable': 'cause', 'condition': 'occurs'}], 9 | 'expected': [{'id': 'e', 'variable': 'effect', 'condition': 'happens'}], 10 | 'cases': [ 11 | {'c': False, 'e': False}, 12 | {'c': True, 'e': True} 13 | ] 14 | } 15 | deserialized = from_dict(suite_dict) 16 | 17 | cause = Parameter(id='c', variable='cause', condition='occurs') 18 | effect = Parameter(id='e', variable='effect', condition='happens') 19 | cases=[ 20 | {'c': False, 'e': False}, 21 | {'c': True, 'e': True} 22 | ] 23 | expected = Suite(conditions=[cause], expected=[effect], cases=cases) 24 | 25 | assert deserialized == expected -------------------------------------------------------------------------------- /test/converters/sentencetolabels/labeler/test_comma_resolver.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src import model_locator 4 | from src.converters.sentencetolabels.labeler import Labeler 5 | from src.data.labels import Label, SubLabel 6 | 7 | @pytest.fixture(scope="module") 8 | def labeler() -> Labeler: 9 | return Labeler(model_path=model_locator.LABELING, useGPU=False) 10 | 11 | @pytest.mark.integration 12 | def test_comma(labeler: Labeler): 13 | sentence = "When deploying, configuring and maintaining the system then the roles must be clear." 14 | 15 | labels = labeler.label(sentence) 16 | variables: list[SubLabel] = [label for label in labels if label.name=="Variable"] 17 | 18 | assert len(variables) == 2 19 | 20 | # make sure each of the two variables has the correct position 21 | assert variables[0].begin == 44 22 | assert variables[0].end == 54 23 | 24 | assert variables[1].begin == 60 25 | assert variables[1].end == 69 -------------------------------------------------------------------------------- /test/converters/graphtotestsuite/test_testsuiteconverter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.converters.graphtotestsuite.testsuiteconverter import convert 4 | 5 | from src.util.loader import load_sentence 6 | import src.util.constants as constants 7 | 8 | from src.data.test import Suite 9 | 10 | 11 | @pytest.fixture 12 | def sentence(id: str): 13 | _, _, _, graph, testsuite = load_sentence( 14 | filename=f'{constants.SENTENCES_PATH}/sentence-{id}.json') 15 | return { 16 | 'graph': graph, 17 | 'testsuite': testsuite 18 | } 19 | 20 | 21 | @pytest.mark.system 22 | @pytest.mark.parametrize('id', ['1', '1b', '1c', '2', '3', '4', '5', '6', '6b', '7', '8', '10', '11', '12', '13', '14', '16', '17', '18']) 23 | def test_system(sentence): 24 | """Automatically generate a test suite from a graph and compare it to a manually generated one.""" 25 | testsuite: Suite = convert(sentence['graph']) 26 | assert sentence['testsuite'] == testsuite 27 | -------------------------------------------------------------------------------- /test/data/labels/test_labels.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.labels import EventLabel, SubLabel 4 | 5 | @pytest.mark.unit 6 | def test_create_eventlabel_emptychildren(): 7 | el = EventLabel(id='T1', name='Cause1', begin=1, end=5) 8 | 9 | assert len(el.children) == 0 10 | 11 | @pytest.mark.unit 12 | def test_create_eventlabel_addchild(): 13 | el = EventLabel(id='T1', name='Cause1', begin=1, end=5) 14 | l = SubLabel(id='T2', name='Condition', begin=1, end=3) 15 | 16 | el.add_child(l) 17 | assert len(el.children) == 1 18 | 19 | @pytest.mark.unit 20 | def test_create_eventlabel_addchild(): 21 | el = EventLabel(id='T1', name='Cause1', begin=1, end=5) 22 | l = SubLabel(id='T2', name='Condition', begin=1, end=3) 23 | 24 | el.add_child(l) 25 | assert l.parent == el 26 | 27 | @pytest.mark.unit 28 | def test_create_sublabel(): 29 | l = SubLabel(id='T2', name='Condition', begin=1, end=3) 30 | 31 | assert l.parent == None -------------------------------------------------------------------------------- /test/test_model_locator.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from src import model_locator 3 | 4 | 5 | @pytest.mark.unit 6 | def test_locate_labeling_model(): 7 | model_path = model_locator.LABELING 8 | assert model_path.endswith('.ckpt') 9 | 10 | 11 | @pytest.mark.unit 12 | def test_locate_classification_model(): 13 | model_path = model_locator.CLASSIFICATION 14 | assert model_path.endswith('.bin') 15 | 16 | 17 | @pytest.mark.unit 18 | def test_locate_labeling_model_in_container(mocker): 19 | mocker.patch("os.path.isfile", return_value=True) 20 | 21 | model_path = model_locator.__load_model_env('MODEL_CONTAINER_DEV') 22 | assert model_path == 'container/cira-labeler.ckpt' 23 | 24 | 25 | @pytest.mark.unit 26 | def test_locate_classification_model_in_container(mocker): 27 | mocker.patch("os.path.isfile", return_value=False) 28 | 29 | with pytest.raises(NameError): 30 | model_locator.__load_model_env('MODEL_CONTAINER_DEV') 31 | 32 | 33 | @pytest.mark.unit 34 | def test_locate_unkown_env(): 35 | with pytest.raises(NameError): 36 | model_locator.__load_model_env('NOT_EXISTING_ENV') 37 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | with open(file='README.md', mode='r') as readme_handler: 4 | long_description = readme_handler.read() 5 | 6 | setup( 7 | name='cira', 8 | author='Julian Frattini', 9 | author_email='juf@bth.se', 10 | version='0.9.7', 11 | description='Implementation of the Causality in Requirements Artifacts (CiRA) functionality', 12 | long_description=long_description, 13 | long_description_content_type='text/markdown', 14 | keywords=['nlp'], 15 | url='https://github.com/JulianFrattini/cira', 16 | python_requires='>=3.10', 17 | install_requires=[ 18 | 'numpy==1.23', 19 | 'python-dotenv==0.20', 20 | 'pytorch_lightning==1.7', 21 | 'tabulate==0.8.10', 22 | 'torch==1.12', 23 | 'transformers==4.10', 24 | 'uvicorn==0.18.3', 25 | 'fastapi==0.85.0', 26 | 'pydantic==1.10.2' 27 | ], 28 | extras_require={ 29 | 'dev': [ 30 | 'pytest==7.1.3', 31 | 'pytest-cov==3.0', 32 | 'pytest-env==0.8.1', 33 | 'pytest-mock==3.10.0' 34 | ], 35 | }, 36 | ) 37 | 38 | -------------------------------------------------------------------------------- /test/converters/labelstograph/test_graphconverter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.converters.labelstograph.graphconverter import GraphConverter 4 | from src.converters.labelstograph.eventresolver import SimpleResolver 5 | 6 | from src.util.loader import load_sentence 7 | from src.util import constants 8 | 9 | 10 | @pytest.fixture(scope="module") 11 | def sut() -> GraphConverter: 12 | return GraphConverter(eventresolver=SimpleResolver()) 13 | 14 | 15 | @pytest.fixture 16 | def sentence(id: str): 17 | _, sentence, labels, graph, _ = load_sentence( 18 | filename=f'{constants.SENTENCES_PATH}/sentence-{id}.json') 19 | return { 20 | 'text': sentence, 21 | 'labels': labels, 22 | 'graph': graph 23 | } 24 | 25 | 26 | @pytest.mark.system 27 | @pytest.mark.parametrize('id', ['1', '1b', '1c', '2', '3', '4', '5', '6', '6b', '7', '8', '10', '11', '12', '13', '14', '16', '17', '18']) 28 | def test_graphconverter(sentence, sut: GraphConverter): 29 | """Test that a graph generated by the graphconverter is equal to a manually generated one""" 30 | graph = sut.generate_graph(sentence['text'], sentence['labels']) 31 | assert sentence['graph'] == graph 32 | -------------------------------------------------------------------------------- /test/data/graph/IntermediateNode/test_get_parents_ordered_by_precedence.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.graph import EventNode, IntermediateNode 4 | 5 | @pytest.mark.integration 6 | def test_conjuction_disjunction(): 7 | event = EventNode(id='e', labels=None) 8 | i1 = IntermediateNode(id='conj', conjunction=True) 9 | i2 = IntermediateNode(id='disj', conjunction=False) 10 | i1.add_incoming(event) 11 | i2.add_incoming(event) 12 | 13 | ordered_parents: list[IntermediateNode] = event.get_parents_ordered_by_precedence() 14 | 15 | assert len(ordered_parents) == 2 16 | assert ordered_parents[0].conjunction == False 17 | assert ordered_parents[1].conjunction == True 18 | 19 | @pytest.mark.integration 20 | def test_overruling_disjunction_conjunction(): 21 | event = EventNode(id='e', labels=None) 22 | i1 = IntermediateNode(id='disj', conjunction=False, precedence=True) 23 | i2 = IntermediateNode(id='conj', conjunction=True) 24 | i1.add_incoming(event) 25 | i2.add_incoming(event) 26 | 27 | ordered_parents: list[IntermediateNode] = event.get_parents_ordered_by_precedence() 28 | 29 | assert len(ordered_parents) == 2 30 | assert ordered_parents[0].conjunction == True 31 | assert ordered_parents[1].conjunction == False -------------------------------------------------------------------------------- /test/converters/sentencetolabels/labelrecovery/test_label_exceptive_clause.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.converters.sentencetolabels.labelrecovery import label_exceptive_clauses 4 | 5 | from src.data.labels import Label, EventLabel, SubLabel 6 | 7 | @pytest.mark.integration 8 | def test_missing(): 9 | sentence = "Unless the button is pressed." 10 | labels = [ 11 | SubLabel(id='L1', name='Variable', begin=7, end=17), 12 | SubLabel(id='L2', name='Condition', begin=18, end=28), 13 | EventLabel(id='L3', name='Cause1', begin=7, end=28) 14 | ] 15 | 16 | additional_labels = label_exceptive_clauses(sentence, labels) 17 | 18 | expected = [SubLabel(id='AEX0', name='Negation', begin=0, end=6)] 19 | assert additional_labels == expected 20 | 21 | @pytest.mark.integration 22 | def test_existing(): 23 | sentence = "Unless the button is pressed." 24 | labels = [ 25 | SubLabel(id='L0', name='Negation', begin=0, end=6), 26 | SubLabel(id='L1', name='Variable', begin=7, end=17), 27 | SubLabel(id='L2', name='Condition', begin=18, end=28), 28 | EventLabel(id='L3', name='Cause1', begin=7, end=28), 29 | ] 30 | 31 | additional_labels = label_exceptive_clauses(sentence, labels) 32 | 33 | assert additional_labels == [] -------------------------------------------------------------------------------- /test/converters/labelstograph/eventresolver/test_join_event_labels.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.converters.labelstograph.eventresolver import join_event_labels 4 | 5 | from src.data.labels import EventLabel 6 | 7 | @pytest.mark.unit 8 | def test_no_join(): 9 | c1 = EventLabel(id='e1', name='Cause1', begin=0, end=1) 10 | c2 = EventLabel(id='e2', name='Cause2', begin=2, end=3) 11 | e1 = EventLabel(id='e3', name='Effect1', begin=4, end=5) 12 | 13 | c1.set_successor(c2, junctor='AND') 14 | c2.set_successor(e1, junctor=None) 15 | 16 | event_labels: list[EventLabel] = [c1, c2, e1] 17 | 18 | result = join_event_labels(event_labels=event_labels) 19 | expected_result = [[c1], [c2], [e1]] 20 | 21 | assert result == expected_result 22 | 23 | @pytest.mark.unit 24 | def test_join(): 25 | c1_1 = EventLabel(id='e1', name='Cause1', begin=0, end=1) 26 | c1_2 = EventLabel(id='e2', name='Cause1', begin=2, end=3) 27 | e1 = EventLabel(id='e3', name='Effect1', begin=4, end=5) 28 | 29 | c1_1.set_successor(c1_2, junctor=None) 30 | c1_2.set_successor(e1, junctor=None) 31 | 32 | event_labels: list[EventLabel] = [c1_1, c1_2, e1] 33 | 34 | result = join_event_labels(event_labels=event_labels) 35 | expected_result = [[c1_1, c1_2], [e1]] 36 | 37 | assert result == expected_result -------------------------------------------------------------------------------- /src/util/constants.py: -------------------------------------------------------------------------------- 1 | SENTENCES_PATH = './static/sentences' 2 | 3 | UNLESS = 'unless' 4 | EXCEPTIVE_CLAUSES = [UNLESS] 5 | 6 | # Event 7 | CAUSE = 'Cause' 8 | EFFECT = 'Effect' 9 | 10 | def is_event(s): 11 | return s in (CAUSE, EFFECT) 12 | 13 | # Direction 14 | PREDECESSOR = 'predecessor' 15 | SUCCESSOR = 'successor' 16 | TARGET = 'target' 17 | ORIGIN = 'origin' 18 | 19 | # Junctor 20 | CONJUNCTION = 'Conjunction' 21 | DISJUNCTION = 'Disjunction' 22 | 23 | def is_junctor(s): 24 | return s in (CONJUNCTION, DISJUNCTION) 25 | 26 | # Logic 27 | OR = 'OR' 28 | AND = 'AND' 29 | POR = 'POR' 30 | 31 | # Attribute 32 | VARIABLE = 'Variable' 33 | CONDITION = 'Condition' 34 | 35 | NOTRELEVANT = 'notrelevant' 36 | 37 | NEGATION = 'Negation' 38 | 39 | LABEL_IDS = ['NOT_RELEVANT', 40 | CAUSE.upper()+'_1', CAUSE.upper()+'_2', CAUSE.upper()+'_3', 41 | EFFECT.upper()+'_1', EFFECT.upper()+'_2', EFFECT.upper()+'_3', 42 | AND, OR, 43 | VARIABLE.upper(), CONDITION.upper(), NEGATION.upper()] 44 | 45 | LABEL_IDS_VERBOSE = [NOTRELEVANT, 46 | CAUSE+'1', CAUSE+'2', CAUSE+'3', 47 | EFFECT+'1', EFFECT+'2', EFFECT+'3', 48 | CONJUNCTION, DISJUNCTION, 49 | VARIABLE, CONDITION, 50 | NEGATION] 51 | -------------------------------------------------------------------------------- /test/converters/graphtotestsuite/testsuiteconverter/test_map_node_to_parameter.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.converters.graphtotestsuite.testsuiteconverter import map_node_to_parameter as mntp 4 | from src.converters.graphtotestsuite.testsuiteconverter import generate_parameters as gp 5 | from src.converters.graphtotestsuite.testsuiteconverter import get_expected_outcome as geo 6 | 7 | from src.data.graph import EventNode 8 | 9 | @pytest.mark.integration 10 | @pytest.mark.parametrize('effect_edges_negated', [[False], [True], [False, False], [False, True], [True, False], [True, True]]) 11 | def test_test(effect_edges_negated): 12 | root = EventNode(id='C1', variable='the developer', condition='does not pey attention') 13 | 14 | events = [] 15 | for index, effect_edge in enumerate(effect_edges_negated): 16 | effect = EventNode(id=f'E{index}', variable=f'effect {index}', condition='occurs') 17 | effect.add_incoming(child=root, negated=(effect_edge)) 18 | events.append(effect) 19 | 20 | parameters = gp(nodes=events) 21 | expected_outcome = geo(root_node_evaluation=True, effects=events) 22 | configuration = mntp(configuration=expected_outcome, parameters_map=parameters) 23 | 24 | # construct the expected value 25 | expected = {f'P{index}': not ee for index, ee in enumerate(effect_edges_negated)} 26 | 27 | assert configuration == expected -------------------------------------------------------------------------------- /test/data/labels/test_get_label_by_type_and_position.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.labels import Label, EventLabel, SubLabel 4 | from src.data.labels import get_label_by_type_and_position as glbtap 5 | 6 | 7 | @pytest.mark.unit 8 | def test_get_label_exists(): 9 | labels = [SubLabel(id='L1', name='Variable', begin=0, end=10)] 10 | returned = glbtap(labels, 'Variable', 0, 10) 11 | 12 | assert returned == labels[0] 13 | 14 | 15 | @pytest.mark.unit 16 | def test_get_label_not_exists(): 17 | labels = [SubLabel(id='L1', name='Variable', begin=0, end=10)] 18 | returned = glbtap(labels, 'Condition', 0, 10) 19 | 20 | assert returned == None 21 | 22 | 23 | @pytest.mark.unit 24 | def test_get_label_multiple_exist_label(): 25 | labels = [ 26 | SubLabel(id='L1', name='Condition', begin=0, end=10), 27 | SubLabel(id='L2', name='Condition', begin=0, end=10) 28 | ] 29 | returned = glbtap(labels, 'Condition', 0, 10) 30 | 31 | assert returned == labels[0] 32 | 33 | 34 | @pytest.mark.unit 35 | def test_get_label_multiple_exist_message(capsys): 36 | labels = [ 37 | SubLabel(id='L1', name='Condition', begin=0, end=10), 38 | SubLabel(id='L2', name='Condition', begin=0, end=10) 39 | ] 40 | glbtap(labels, 'Condition', 0, 10) 41 | message = capsys.readouterr().out 42 | assert message == 'Warning: searching for a Condition label at position [0, 10] yielded multiple results.\n' 43 | -------------------------------------------------------------------------------- /test/data/graph/Node/test_add_delete_incoming.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.graph import IntermediateNode, EventNode 4 | from src.data.labels import EventLabel, SubLabel 5 | 6 | @pytest.mark.unit 7 | def test_addchild(): 8 | i = IntermediateNode(id='I1', conjunction=False) 9 | e = EventNode(id='E1') 10 | 11 | i.add_incoming(e) 12 | assert e.outgoing[0].target == i 13 | 14 | @pytest.mark.unit 15 | def test_removechild(): 16 | i = IntermediateNode(id='I1', conjunction=False) 17 | e = EventNode(id='E1') 18 | i.add_incoming(e) 19 | 20 | i.remove_incoming(e) 21 | assert len(e.outgoing) == 0 22 | 23 | @pytest.mark.unit 24 | def test_addchildren(): 25 | i = IntermediateNode(id='I1', conjunction=False) 26 | e1 = EventNode(id='E1') 27 | e2 = EventNode(id='E2') 28 | 29 | i.add_incoming(e1) 30 | i.add_incoming(e2) 31 | 32 | assert len(i.incoming) == 2 33 | 34 | @pytest.mark.unit 35 | def test_removeonechild(): 36 | i = IntermediateNode(id='I1', conjunction=False) 37 | e1 = EventNode(id='E1') 38 | e2 = EventNode(id='E2') 39 | 40 | i.add_incoming(e1) 41 | i.add_incoming(e2) 42 | 43 | i.remove_incoming(e1) 44 | 45 | assert len(i.incoming) == 1 46 | 47 | @pytest.mark.unit 48 | def test_negatednode(): 49 | l1 = EventLabel(id='L1', name='Cause1', begin=0, end=10) 50 | l1.add_child(SubLabel(id='L2', name='Negation', begin=0, end=2)) 51 | e1 = EventNode(id='E1', labels=[l1]) 52 | 53 | assert e1.is_negated() == True -------------------------------------------------------------------------------- /src/util/loader.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Tuple 3 | 4 | from src.data.labels import from_dict as labels_from_dict 5 | from src.data.graph import from_dict as graph_from_dict 6 | from src.data.test import from_dict as testsuite_from_dict 7 | 8 | from src.data.labels import Label 9 | from src.data.graph import Graph 10 | from src.data.test import Suite 11 | 12 | def load_sentence(filename: str) -> Tuple[object, str, list[Label], Graph, Suite]: 13 | """Load a sentence from a json file and convert the information into the internal representation of the pipeline. The json file is assumed to be in the format that is used for the static test files (currently to be found at the location specified in src.util.constants.SENTENCES_PATH). 14 | 15 | parameters: 16 | filename -- location of the json file 17 | 18 | returns: 19 | file -- pure file as read from the disc 20 | sentence -- the literal sentence 21 | labels -- list of labels as manually annotated on the sentence 22 | graph -- cause-effect graph representing the sentence 23 | testsuite -- test suite containing all parameters and all relevant test cases """ 24 | with open(filename, 'r') as f: 25 | file = json.load(f) 26 | 27 | sentence: str = file['sentence'] 28 | labels: list[Label] = labels_from_dict(serialized=file['labels']) 29 | graph: Graph = graph_from_dict(dict_graph=file['graph']) 30 | testsuite: Suite = testsuite_from_dict(dict_suite=file['testsuite']) 31 | 32 | return (file, sentence, labels, graph, testsuite) -------------------------------------------------------------------------------- /test/data/graph/test_permute_configurations.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.graph import permute_configurations 4 | 5 | @pytest.mark.unit 6 | def test_permute_none(): 7 | # if only one set of configurations is given, there is no need for permutation 8 | configurations = [[{'n1': True, 'n2': False}, {'n1': False, 'n2': True}]] 9 | permutations = permute_configurations(inc_configs=configurations) 10 | 11 | assert permutations == configurations[0] 12 | 13 | @pytest.mark.unit 14 | def test_permute_one(): 15 | # if there are two sets of configurations, but one is just a singular configuration, add this configuration to the other ones 16 | configurations = [[{'n1': True, 'n2': False}, {'n1': False, 'n2': True}], [{'n3': True}]] 17 | permutations = permute_configurations(inc_configs=configurations) 18 | 19 | assert permutations == [{'n1': True, 'n2': False, 'n3': True}, {'n1': False, 'n2': True, 'n3': True}] 20 | 21 | @pytest.mark.unit 22 | def test_permute_two(): 23 | # if there are two sets of configurations, with at least two configurations, expect the product of these configurations 24 | configurations = [[{'n1': True, 'n2': False}, {'n1': False, 'n2': True}], [{'n3': True}, {'n3': False}]] 25 | permutations = permute_configurations(inc_configs=configurations) 26 | 27 | # TODO this equality is dependent on the order of the elements in the list. Improve the == 28 | assert permutations == [{'n1': True, 'n2': False, 'n3': True}, {'n1': True, 'n2': False, 'n3': False}, {'n1': False, 'n2': True, 'n3': True}, {'n1': False, 'n2': True, 'n3': False}] -------------------------------------------------------------------------------- /.github/workflows/docker-image-app.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker app image 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | workflow_dispatch: 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | IMAGE_NAME: ${{ github.repository }} 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@v2 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Get CiRA version 31 | run: | 32 | CIRA_VERSION=$(python setup.py --version) 33 | echo "CIRA_VERSION=$CIRA_VERSION" >> $GITHUB_ENV 34 | 35 | - name: Extract metadata (tags, labels) for Docker 36 | id: meta 37 | uses: docker/metadata-action@v4 38 | with: 39 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 40 | labels: | 41 | org.opencontainers.image.version=${{ env.CIRA_VERSION }} 42 | tags: | 43 | latest 44 | ${{ env.CIRA_VERSION }} 45 | 46 | - name: Build and push Docker image 47 | uses: docker/build-push-action@v3 48 | with: 49 | context: . 50 | file: Dockerfile 51 | push: true 52 | tags: ${{ steps.meta.outputs.tags}} 53 | labels: ${{ steps.meta.outputs.labels }} 54 | -------------------------------------------------------------------------------- /.github/workflows/docker-image-dev.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker dev image 2 | 3 | on: 4 | workflow_dispatch: 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | IMAGE_NAME: ${{ github.repository }}-dev 9 | 10 | jobs: 11 | build-and-push-image: 12 | runs-on: ubuntu-latest 13 | permissions: 14 | contents: read 15 | packages: write 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Cache model files 22 | id: cache-models 23 | uses: actions/cache@v3 24 | with: 25 | path: model/ 26 | key: zenodo-record-7186287 27 | restore-keys: | 28 | zenodo-record-7186287 29 | 30 | - name: Download model files 31 | run: | 32 | chmod +x download-models.sh 33 | ./download-models.sh 34 | find model -name "*.zip" -delete 35 | 36 | - name: Log in to the Container registry 37 | uses: docker/login-action@v2 38 | with: 39 | registry: ${{ env.REGISTRY }} 40 | username: ${{ github.actor }} 41 | password: ${{ secrets.GITHUB_TOKEN }} 42 | 43 | - name: Extract metadata (tags, labels) for Docker 44 | id: meta 45 | uses: docker/metadata-action@v4 46 | with: 47 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 48 | tags: latest 49 | 50 | - name: Build and push Docker image 51 | uses: docker/build-push-action@v3 52 | with: 53 | context: . 54 | file: Dockerfile.dev 55 | push: true 56 | tags: ${{ steps.meta.outputs.tags}} 57 | labels: ${{ steps.meta.outputs.labels }} 58 | -------------------------------------------------------------------------------- /test/converters/sentencetolabels/labelingconverter/test_merge_labels.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import src.converters.sentencetolabels.labelingconverter as lc 4 | from src.converters.sentencetolabels.labelingconverter import TokenLabel 5 | 6 | @pytest.mark.unit 7 | def test_merge1(): 8 | sentence = "If the red button is pressed" 9 | token_labels = [ 10 | TokenLabel(begin=3, end=6, event=True, name='Cause1'), 11 | TokenLabel(begin=3, end=6, event=False, name='Variable'), 12 | TokenLabel(begin=7, end=10, event=True, name='Cause1'), 13 | TokenLabel(begin=7, end=10, event=False, name='Variable'), 14 | TokenLabel(begin=11, end=17, event=True, name='Cause1'), 15 | TokenLabel(begin=11, end=17, event=False, name='Variable'), 16 | TokenLabel(begin=18, end=20, event=True, name='Cause1'), 17 | TokenLabel(begin=18, end=20, event=False, name='Condition'), 18 | TokenLabel(begin=21, end=28, event=True, name='Cause1'), 19 | TokenLabel(begin=21, end=28, event=False, name='Condition') 20 | ] 21 | label_ids_verbose = ['notrelevant', 'Cause1', 'Cause2', 'Cause3', 'Effect1', 'Effect2', 'Effect3', 'Conjunction', 'Disjunction', 'Variable', 'Condition', 'Negation'] 22 | 23 | labels = lc.merge_labels(token_labels=token_labels) 24 | 25 | cause1 = [label for label in labels if label.name=='Cause1'] 26 | assert len(cause1) == 1 27 | assert cause1[0].begin == 3 28 | assert cause1[0].end == 28 29 | 30 | variable = [label for label in labels if label.name=='Variable'] 31 | assert len(variable) == 1 32 | assert variable[0].begin == 3 33 | assert variable[0].end == 17 34 | 35 | condition = [label for label in labels if label.name=='Condition'] 36 | assert len(condition) == 1 37 | assert condition[0].begin == 18 38 | assert condition[0].end == 28 -------------------------------------------------------------------------------- /test/api/service/test_recover_labels.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import unittest.mock as mock 3 | from unittest.mock import patch 4 | 5 | from src.api.service import CiRAServiceImpl 6 | from src.data.labels import SubLabel 7 | 8 | sentence = "If the button is pressed than the system shuts down." 9 | 10 | 11 | @pytest.fixture(scope="module") 12 | @patch('src.api.service.CiRAConverter', autospec=True) 13 | def isolatedService(converter) -> CiRAServiceImpl: 14 | mockedConverter = mock.MagicMock() 15 | mockedConverter.label.return_value = [SubLabel(id='L1', name='Variable', begin=3, end=14)] 16 | converter.return_value = mockedConverter 17 | 18 | service = CiRAServiceImpl(model_classification=None, model_labeling=None) 19 | return service 20 | 21 | 22 | @pytest.mark.unit 23 | def test_recover_none(isolatedService): 24 | labels = isolatedService.get_deserialized_labels(sentence, labels=None) 25 | expected = [SubLabel(id='L1', name='Variable', begin=3, end=14)] 26 | assert labels == expected 27 | 28 | 29 | @pytest.mark.unit 30 | def test_recover_empty(isolatedService): 31 | labels = isolatedService.get_deserialized_labels(sentence, labels=[]) 32 | expected = [SubLabel(id='L1', name='Variable', begin=3, end=14)] 33 | assert labels == expected 34 | 35 | 36 | @pytest.mark.unit 37 | def test_recover_serialized(isolatedService): 38 | labels = isolatedService.get_deserialized_labels(sentence, labels=[{'id': 'L1', 'name': 'Variable', 'begin': 3, 'end': 14, 'parent': None}]) 39 | expected = [SubLabel(id='L1', name='Variable', begin=3, end=14)] 40 | assert labels == expected 41 | 42 | 43 | @pytest.mark.unit 44 | def test_recover_existing(isolatedService): 45 | labels = isolatedService.get_deserialized_labels(sentence, labels=[SubLabel(id='L1', name='Variable', begin=3, end=14)]) 46 | expected = [SubLabel(id='L1', name='Variable', begin=3, end=14)] 47 | assert labels == expected 48 | 49 | -------------------------------------------------------------------------------- /test/converters/labelstograph/eventresolver/test_neighbor_detector.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.labels import EventLabel 4 | 5 | from src.converters.labelstograph.eventresolver import get_all_neighbors_of_type as neighbors 6 | 7 | @pytest.fixture 8 | def events() -> list[EventLabel]: 9 | c1 = EventLabel('L1', name='Cause1', begin=0, end=1) 10 | c2 = EventLabel('L2', name='Cause2', begin=1, end=2) 11 | c3 = EventLabel('L3', name='Cause3', begin=2, end=3) 12 | e1 = EventLabel('L4', name='Effect1', begin=3, end=4) 13 | e2 = EventLabel('L5', name='Effect2', begin=4, end=5) 14 | e3 = EventLabel('L6', name='Effect3', begin=5, end=6) 15 | 16 | c1.set_successor(c2, junctor='AND') 17 | c2.set_successor(c3, junctor='AND') 18 | c3.set_successor(e1, junctor='AND') 19 | e1.set_successor(e2, junctor='AND') 20 | e2.set_successor(e3, junctor='AND') 21 | 22 | return [c1, c2, c3, e1, e2, e3] 23 | 24 | @pytest.mark.unit 25 | def test_preceeding_causes(events): 26 | candidates = neighbors(startlabel=events[1], direction='predecessor', type='Cause') 27 | 28 | assert len(candidates) == 1 29 | assert candidates[0] == events[0] 30 | 31 | @pytest.mark.unit 32 | def test_succeeding_causes(events): 33 | candidates = neighbors(startlabel=events[1], direction='successor', type='Cause') 34 | 35 | assert len(candidates) == 1 36 | assert candidates[0] == events[2] 37 | 38 | @pytest.mark.unit 39 | def test_preceeding_effects(events): 40 | candidates = neighbors(startlabel=events[1], direction='predecessor', type='Effect') 41 | 42 | assert len(candidates) == 0 43 | 44 | @pytest.mark.unit 45 | def test_succeeding_effects(events): 46 | candidates = neighbors(startlabel=events[1], direction='successor', type='Effect') 47 | 48 | assert len(candidates) == 3 49 | assert candidates[0] == events[3] 50 | assert candidates[1] == events[4] 51 | assert candidates[2] == events[5] -------------------------------------------------------------------------------- /test/converters/sentencetolabels/labelingconverter/test_junctors_between.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.labels import Label, EventLabel, SubLabel 4 | 5 | from src.converters.sentencetolabels.labelingconverter import get_junctors_between 6 | 7 | @pytest.mark.unit 8 | def test_junctor_exist(): 9 | labels: list[Label] = [ 10 | EventLabel(id='L1', name='Cause1', begin=0, end=10), 11 | SubLabel(id='L2', name='Conjunction', begin=11, end=14), 12 | EventLabel(id='L3', name='Cause2', begin=15, end=25), 13 | ] 14 | 15 | junctors = get_junctors_between(labels=labels, first=labels[0], second=labels[2]) 16 | assert junctors[0].name == 'Conjunction' 17 | 18 | @pytest.mark.unit 19 | def test_junctor_notexist(): 20 | labels: list[Label] = [ 21 | EventLabel(id='L1', name='Cause1', begin=0, end=10), 22 | EventLabel(id='L3', name='Cause2', begin=15, end=25), 23 | ] 24 | 25 | junctors = get_junctors_between(labels=labels, first=labels[0], second=labels[1]) 26 | assert len(junctors) == 0 27 | 28 | @pytest.mark.unit 29 | def test_junctor_outside(): 30 | labels: list[Label] = [ 31 | EventLabel(id='L1', name='Cause1', begin=0, end=10), 32 | SubLabel(id='L2', name='Conjunction', begin=22, end=25), 33 | EventLabel(id='L3', name='Cause2', begin=11, end=21), 34 | ] 35 | 36 | junctors = get_junctors_between(labels=labels, first=labels[0], second=labels[2]) 37 | assert len(junctors) == 0 38 | 39 | @pytest.mark.unit 40 | def test_junctor_two(): 41 | labels: list[Label] = [ 42 | EventLabel(id='L1', name='Cause1', begin=0, end=10), 43 | SubLabel(id='L2', name='Conjunction', begin=11, end=14), 44 | SubLabel(id='L3', name='Disjunction', begin=15, end=17), 45 | EventLabel(id='L4', name='Cause2', begin=18, end=28), 46 | ] 47 | 48 | junctors = get_junctors_between(labels=labels, first=labels[0], second=labels[3]) 49 | assert len(junctors) == 2 -------------------------------------------------------------------------------- /src/converters/sentencetolabels/labelrecovery.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import src.util.constants as consts 4 | from src.data.labels import Label, SubLabel, get_label_by_type_and_position 5 | 6 | 7 | def recover_labels(sentence: str, labels: list[Label]) -> list[Label]: 8 | """The capabilities of the BERT-based sentence labeler are limited. Because some sentence structures are more rare than others (e.g., exceptive clauses), the labeler might miss important information. This method manually recovers certain labels according to specific patterns and updates the list of labels currently associated with the sentence 9 | """ 10 | # recover missing labels 11 | for add_labels in [label_exceptive_clauses]: 12 | labels += add_labels(sentence, labels) 13 | 14 | return labels 15 | 16 | def label_exceptive_clauses(sentence: str, labels: list[Label]) -> list[SubLabel]: 17 | """Identify all instances of exceptive clauses (determined by the list of words above) that have not been labeled. Because exceptive clauses like this are very rare they have apparently not been picked up by the BERT-based labeler. Because they convey important information ("Unless A then B" translates to "If not A then B") they need to be recovered. This method generates a negation for each exceptive clause that does not yet contain one. 18 | 19 | parameters: 20 | sentence -- natural language sentence 21 | labels -- list of labels already associated with the sentence 22 | 23 | returns: list of labels for previously unlabeled exceptive clauses""" 24 | 25 | additional_labels: list[SubLabel] = [] 26 | 27 | exceptive_instances = [] 28 | for exclause in consts.EXCEPTIVE_CLAUSES: 29 | iter = re.finditer(exclause, sentence.lower()) 30 | exceptive_instances += [m.span() for m in iter] 31 | 32 | for index, instance in enumerate(exceptive_instances): 33 | label = get_label_by_type_and_position(labels, type=consts.NEGATION, begin=instance[0], end=instance[1]) 34 | if label is None: 35 | additional_labels.append( 36 | SubLabel(id=f'AEX{index}', name=consts.NEGATION, begin=instance[0], end=instance[1]) 37 | ) 38 | 39 | return additional_labels 40 | 41 | -------------------------------------------------------------------------------- /test/converters/labelstograph/eventresolver/test_get_attribute_of_eventlabel_group.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.converters.labelstograph.eventresolver import get_attribute_of_eventlabel_group 4 | 5 | from src.data.labels import EventLabel, SubLabel 6 | 7 | @pytest.mark.integration 8 | def test_single_label(): 9 | sentence: str = "When the red button is pushed the system shuts down." 10 | 11 | c1 = EventLabel(id='L1', name='Cause1', begin=5, end=29) 12 | c1.add_child(SubLabel(id='L2', name='Variable', begin=5, end=19)) 13 | event_group = [c1] 14 | 15 | result = get_attribute_of_eventlabel_group(event_group, attribute="Variable", sentence=sentence) 16 | 17 | assert result == 'the red button' 18 | 19 | @pytest.mark.integration 20 | def test_spread_event_label(): 21 | sentence: str = "Users which are older than 18 years, are allowed to drive." 22 | 23 | c1_1 = EventLabel(id='L1', name='Cause1', begin=0, end=5) 24 | c1_1.add_child(SubLabel(id='L2', name='Variable', begin=0, end=5)) 25 | c1_2 = EventLabel(id='L3', name='Cause1', begin=12, end=35) 26 | c1_2.add_child(SubLabel(id='L4', name='Condition', begin=12, end=35)) 27 | 28 | c1_1.set_successor(c1_2, junctor='MERGE') 29 | event_group = [c1_1, c1_2] 30 | 31 | variable = get_attribute_of_eventlabel_group(event_group, attribute='Variable', sentence=sentence) 32 | condition = get_attribute_of_eventlabel_group(event_group, attribute='Condition', sentence=sentence) 33 | 34 | assert variable == 'Users' 35 | assert condition == 'are older than 18 years' 36 | 37 | @pytest.mark.integration 38 | def test_spread_sublabel(): 39 | sentence: str = "Data transmission is only possible if the user consented to it." 40 | 41 | c1_1 = EventLabel(id='L1', name='Effect1', begin=0, end=20) 42 | c1_1.add_child(SubLabel(id='L2', name='Variable', begin=0, end=17)) 43 | c1_1.add_child(SubLabel(id='L3', name='Condition', begin=18, end=20)) 44 | c1_2 = EventLabel(id='L4', name='Effect1', begin=26, end=34) 45 | c1_2.add_child(SubLabel(id='L5', name='Condition', begin=26, end=34)) 46 | 47 | c1_1.set_successor(c1_2, junctor='MERGE') 48 | event_group = [c1_1, c1_2] 49 | 50 | variable = get_attribute_of_eventlabel_group(event_group, attribute='Variable', sentence=sentence) 51 | condition = get_attribute_of_eventlabel_group(event_group, attribute='Condition', sentence=sentence) 52 | 53 | assert variable == 'Data transmission' 54 | assert condition == 'is possible' 55 | -------------------------------------------------------------------------------- /test/api/service/test_api_integrate.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src import model_locator 4 | from src.api.service import CiRAServiceImpl 5 | 6 | sentence: str = "If the button is pressed then the system shuts down." 7 | 8 | labels: list[dict] = [ 9 | {'id': 'L0', 'name': 'Cause1', 'begin': 3, 'end': 24, 'successor': { 10 | 'id': 'L1', 'junctor': None}, 'children': ['L2', 'L4']}, 11 | {'id': 'L1', 'name': 'Effect1', 'begin': 30, 'end': 51, 12 | 'successor': None, 'children': ['L3', 'L5']}, 13 | {'id': 'L2', 'name': 'Variable', 'begin': 3, 'end': 13, 'parent': 'L0'}, 14 | {'id': 'L3', 'name': 'Variable', 'begin': 30, 'end': 40, 'parent': 'L1'}, 15 | {'id': 'L4', 'name': 'Condition', 'begin': 14, 'end': 24, 'parent': 'L0'}, 16 | {'id': 'L5', 'name': 'Condition', 'begin': 41, 'end': 51, 'parent': 'L1'}, 17 | ] 18 | 19 | graph = { 20 | 'nodes': [{'id': 'E0', 'variable': 'the button', 'condition': 'is pressed'}, {'id': 'E1', 'variable': 'the system', 'condition': 'shuts down'}], 21 | 'root': 'E0', 22 | 'edges': [{'origin': 'E0', 'target': 'E1', 'negated': False}] 23 | } 24 | 25 | suite = { 26 | 'conditions': [{'id': 'P0', 'variable': 'the button', 27 | 'condition': 'is pressed'}], 28 | 'expected': [{'id': 'P1', 'variable': 'the system', 29 | 'condition': 'shuts down'}], 30 | 'cases': [{'P0': True, 'P1': True}, {'P0': False, 'P1': False}] 31 | } 32 | 33 | 34 | @pytest.fixture(scope="module") 35 | def sut() -> CiRAServiceImpl: 36 | # create the system under test 37 | service = CiRAServiceImpl( 38 | model_classification=model_locator.CLASSIFICATION, model_labeling=model_locator.LABELING) 39 | return service 40 | 41 | 42 | @pytest.mark.integration 43 | def test_classification(sut: CiRAServiceImpl): 44 | classification, confidence = sut.classify(sentence) 45 | assert classification == True 46 | assert confidence > 0.9 47 | 48 | 49 | @pytest.mark.integration 50 | def test_labeling(sut: CiRAServiceImpl): 51 | generated_labels = sut.sentence_to_labels(sentence) 52 | assert generated_labels == labels 53 | 54 | 55 | @pytest.mark.integration 56 | def test_graph(sut: CiRAServiceImpl): 57 | generated_graph = sut.sentence_to_graph(sentence, labels) 58 | assert generated_graph == graph 59 | 60 | 61 | @pytest.mark.integration 62 | def test_testsuite(sut: CiRAServiceImpl): 63 | generated_suite = sut.graph_to_test(graph, sentence) 64 | assert generated_suite == suite 65 | 66 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Model files 132 | model/ 133 | -------------------------------------------------------------------------------- /test/converters/sentencetolabels/test_labeler.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src import model_locator 4 | from src.converters.sentencetolabels.labeler import Labeler 5 | from src.converters.sentencetolabels.labelingconverter import TokenLabel 6 | 7 | from src.data.labels import Label 8 | 9 | from src.util.loader import load_sentence 10 | from src.util import constants 11 | 12 | @pytest.fixture 13 | def sentence(id: str): 14 | _, sentence, labels, _, _ = load_sentence(filename=f'{constants.SENTENCES_PATH}/sentence-{id}.json') 15 | return {'text': sentence, 'labels': labels} 16 | 17 | @pytest.fixture(scope="module") 18 | def labeler(): 19 | return Labeler(model_path=model_locator.LABELING, useGPU=False) 20 | 21 | # currently excluded: sentence 13 (there is a tripple-labeling on "NO defect" with Cause3, Variable, and Negation) 22 | @pytest.mark.system 23 | @pytest.mark.parametrize('id', ['1', '1b', '1c', '2', '3', '4', '5', '6', '6b', '7', '8', '10', '11', '12', '14', '16', '17', '18']) 24 | def test_labeler(sentence, labeler): 25 | """Test that the labeler produces the same labels for a given sentence that a manual annotator would. The manually annotated sentences are stored in .json files and contain both the sentence and the list of manually annotated labels. The test for each sentence is successful if every manual label has exactly one unique equivalent in the list of automatically generated labels. Two labels are equivalent if their (1) name, (2) begin, and (3) end attribute are the same.""" 26 | # abort if the labeler could not be created properly 27 | assert labeler is not None 28 | 29 | # generate the labels 30 | labels_gen = labeler.label(sentence=sentence['text']) 31 | assert labels_gen is not None 32 | 33 | assert equals(expected=sentence['labels'], generated=labels_gen) 34 | 35 | def equals(expected: list[Label], generated: list[Label]) -> bool: 36 | """Determine whether two list of labels are equal. They count as equal if they have the same length and every label in the expected list has exactly one equivalent in the generated list. 37 | 38 | parameters: 39 | expected -- list of expected labels 40 | generated -- list of generated labels 41 | 42 | returns: True if the two lists are equal""" 43 | if len(expected) != len(generated): 44 | return False 45 | 46 | for label in expected: 47 | equivalent = [candidate for candidate in generated if candidate.begin==label.begin and candidate.name==label.name and candidate.end==label.end] 48 | 49 | if len(equivalent) != 1: 50 | return False 51 | 52 | return True -------------------------------------------------------------------------------- /src/classifiers/causalitydetection/causalclassifier.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | 3 | import numpy as np 4 | import torch 5 | import torch.nn.functional as F 6 | from transformers import BertTokenizer 7 | 8 | from src.classifiers.causalitydetection.classificationmodel import CausalClassificationModel 9 | 10 | RANDOM_SEED = 42 11 | np.random.seed(RANDOM_SEED) 12 | torch.manual_seed(RANDOM_SEED) 13 | 14 | CAUSAL = 'causal' 15 | NOT_CAUSAL = 'not causal' 16 | CLASS_NAMES = [NOT_CAUSAL, CAUSAL] 17 | PRE_TRAINED_MODEL_NAME = 'bert-base-cased' 18 | DEVICE_CPU = 'cpu' 19 | DEVICE_GPU = 'cuda:0' 20 | TENSOR_TYPE_PYTORCH = 'pt' 21 | 22 | class CausalClassifier: 23 | def __init__(self, model_path: str): 24 | """Create a causal detector which wraps the pre-trained classification model. 25 | 26 | parameters: 27 | path -- path to the binary file of the pre-trained model""" 28 | 29 | self.device = torch.device(DEVICE_GPU if torch.cuda.is_available() else DEVICE_CPU) 30 | self.tokenizer = BertTokenizer.from_pretrained(PRE_TRAINED_MODEL_NAME) 31 | 32 | self.model = CausalClassificationModel(len(CLASS_NAMES)) 33 | if torch.cuda.is_available(): 34 | self.model.load_state_dict(torch.load(model_path)) 35 | else: 36 | self.model.load_state_dict(torch.load(model_path, map_location=DEVICE_CPU)) 37 | 38 | self.model = self.model.to(self.device) 39 | 40 | def classify(self, sentence: str) -> Tuple[bool, float]: 41 | """Classify a natural language sentence regarding whether it is causal or not. 42 | 43 | parameters: 44 | sentence -- natural language sentence in English 45 | 46 | returns: the classification whether the sentence is causal and the confidence of the classifier""" 47 | 48 | # encode input text 49 | encoded_text = self.tokenizer.encode_plus( 50 | sentence, 51 | max_length=128, 52 | add_special_tokens=True, 53 | return_token_type_ids=False, 54 | padding='max_length', 55 | return_attention_mask=True, 56 | return_tensors=TENSOR_TYPE_PYTORCH, 57 | truncation=True 58 | ) 59 | 60 | # apply classification model 61 | input_ids = encoded_text['input_ids'].to(self.device) 62 | attention_mask = encoded_text['attention_mask'].to(self.device) 63 | output = self.model(input_ids, attention_mask) 64 | _, prediction = torch.max(output, dim=1) 65 | probs = F.softmax(output, dim=1) 66 | 67 | # return both the classification and the confidence 68 | is_causal = (CLASS_NAMES[prediction] == CAUSAL) 69 | confidence = torch.max(probs, dim=1)[0].item() 70 | return (is_causal, confidence) 71 | 72 | -------------------------------------------------------------------------------- /test/app/test_app_isolated.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import pkg_resources 4 | 5 | from fastapi.testclient import TestClient 6 | from fastapi import status 7 | 8 | import app 9 | from src.api.service import CiraServiceMock 10 | 11 | sentence = "If the button is pressed then the system shuts down." 12 | API_URL = 'http://localhost:8000/api/' 13 | 14 | cira_version = pkg_resources.require("cira")[0].version 15 | 16 | @pytest.fixture(scope="module") 17 | def client() -> TestClient: 18 | app.cira = CiraServiceMock() 19 | client = TestClient(app.app) 20 | return client 21 | 22 | 23 | @pytest.mark.unit 24 | def test_routes(client): 25 | response = client.get(f'{API_URL}') 26 | assert response.status_code == status.HTTP_200_OK 27 | 28 | routes = response.json() 29 | assert len(routes) >= 4 30 | 31 | 32 | @pytest.mark.unit 33 | def test_health(client): 34 | response = client.get(f'{API_URL}health') 35 | 36 | assert response.status_code == status.HTTP_200_OK 37 | assert response.json() == { 38 | 'status': 'up', 39 | 'cira-version': cira_version 40 | } 41 | 42 | 43 | @pytest.mark.unit 44 | def test_classification(client): 45 | response = client.put( 46 | f'{API_URL}classify', json={"sentence": sentence}) 47 | 48 | assert response.status_code == status.HTTP_200_OK 49 | assert response.json() == {'causal': True, 'confidence': 0.99} 50 | 51 | 52 | @pytest.mark.unit 53 | def test_labels(client): 54 | response = client.put( 55 | f'{API_URL}label', json={"sentence": sentence}) 56 | 57 | assert response.status_code == status.HTTP_200_OK 58 | assert response.json() == {'labels': [ 59 | {'id': 'L1', 'name': 'Variable', 'begin': 10, 'end': 20, 'parent': None}]} 60 | 61 | 62 | @pytest.mark.unit 63 | def test_graph(client): 64 | response = client.put( 65 | f'{API_URL}graph', json={"sentence": sentence}) 66 | 67 | assert response.status_code == status.HTTP_200_OK 68 | assert response.json() == {'graph': { 69 | 'nodes': [ 70 | {'id': 'c', 'variable': 'the button', 'condition': 'is pressed'}, 71 | {'id': 'e', 'variable': 'the system', 'condition': 'shuts down'} 72 | ], 73 | 'root': 'c', 74 | 'edges': [{'origin': 'c', 'target': 'e', 'negated': False}] 75 | }} 76 | 77 | 78 | @pytest.mark.unit 79 | def test_suite(client): 80 | response = client.put( 81 | f'{API_URL}testsuite', json={"sentence": sentence, "graph": None}) 82 | 83 | assert response.status_code == status.HTTP_200_OK 84 | assert response.json() == {'suite': { 85 | 'conditions': [{'id': 'c', 'variable': 'the button', 'condition': 'is pressed'}], 86 | 'expected': [{'id': 'c', 'variable': 'the system', 'condition': 'shuts down'}], 87 | 'cases': [{'c': True, 'e': True}, {'c': False, 'e': False}] 88 | }} 89 | -------------------------------------------------------------------------------- /test/converters/labelstograph/eventresolver/test_get_events_in_order.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.converters.labelstograph.eventresolver import get_events_in_order 4 | 5 | from src.data.labels import EventLabel 6 | from src.data.graph import EventNode 7 | 8 | @pytest.fixture 9 | def events_with_single_label() -> list[EventLabel]: 10 | c1 = EventLabel('L1', name='Cause1', begin=0, end=1) 11 | c2 = EventLabel('L2', name='Cause2', begin=1, end=2) 12 | c3 = EventLabel('L3', name='Cause3', begin=2, end=3) 13 | e1 = EventLabel('L4', name='Effect1', begin=3, end=4) 14 | e2 = EventLabel('L5', name='Effect2', begin=4, end=5) 15 | e3 = EventLabel('L6', name='Effect3', begin=5, end=6) 16 | 17 | c1.set_successor(c2, None) 18 | c2.set_successor(c3, None) 19 | c3.set_successor(e1, None) 20 | e1.set_successor(e2, None) 21 | e2.set_successor(e3, None) 22 | 23 | return [c1, c2, c3, e1, e2, e3] 24 | 25 | @pytest.mark.integration 26 | def test_L2_variable(events_with_single_label): 27 | node = EventNode(id='N1', labels=[events_with_single_label[1]]) 28 | candidates = get_events_in_order(starting_node=node, attribute='Variable') 29 | assert [c.id for c in candidates] == ['L1', 'L3', 'L4', 'L5', 'L6'] 30 | 31 | 32 | @pytest.mark.integration 33 | def test_L4_condition(events_with_single_label): 34 | node = EventNode(id='N1', labels=[events_with_single_label[3]]) 35 | candidates = get_events_in_order(starting_node=node, attribute='Condition') 36 | assert [c.id for c in candidates] == ['L5', 'L6', 'L3', 'L2', 'L1'] 37 | 38 | @pytest.fixture 39 | def events_with_multiple_label() -> list[EventLabel]: 40 | c1 = EventLabel('L1', name='Cause1', begin=0, end=1) 41 | c2_1 = EventLabel('L2-1', name='Cause2', begin=1, end=2) 42 | c2_2 = EventLabel('L2-2', name='Cause2', begin=2, end=3) 43 | c3 = EventLabel('L3', name='Cause3', begin=4, end=5) 44 | e1 = EventLabel('L4', name='Effect1', begin=5, end=6) 45 | e2_1 = EventLabel('L5-1', name='Effect2', begin=7, end=8) 46 | e2_2 = EventLabel('L5-2', name='Effect2', begin=8, end=9) 47 | e3 = EventLabel('L6', name='Effect3', begin=9, end=10) 48 | 49 | c1.set_successor(c2_1, None) 50 | c2_1.set_successor(c2_2, "MERGE") 51 | c2_2.set_successor(c3, None) 52 | c3.set_successor(e1, None) 53 | e1.set_successor(e2_1, None) 54 | e2_1.set_successor(e2_2, "MERGE") 55 | e2_2.set_successor(e3, None) 56 | 57 | return [c1, c2_1, c2_2, c3, e1, e2_1, e2_2, e3] 58 | 59 | @pytest.mark.integration 60 | def test_L2_1_variable(events_with_multiple_label): 61 | c2_1 = events_with_multiple_label[1] 62 | c2_2 = events_with_multiple_label[2] 63 | node = EventNode(id='N1', labels=[c2_1, c2_2]) 64 | 65 | candidates = get_events_in_order(starting_node=node, attribute='Variable') 66 | assert [c.id for c in candidates] == ['L1', 'L3', 'L4', 'L5-1', 'L5-2', 'L6'] -------------------------------------------------------------------------------- /static/sentences/sentence-1d.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentence": "When opening a window a sound will be played.", 3 | "labels": [ 4 | { 5 | "id": "T2", 6 | "name": "Cause1", 7 | "begin": 5, 8 | "end": 21, 9 | "successor": { 10 | "id": "T5", 11 | "junctor": null 12 | }, 13 | "children": [ 14 | "T3", 15 | "T4" 16 | ] 17 | }, 18 | { 19 | "id": "T3", 20 | "name": "Condition", 21 | "begin": 5, 22 | "end": 12, 23 | "parent": "T2" 24 | }, 25 | { 26 | "id": "T4", 27 | "name": "Variable", 28 | "begin": 13, 29 | "end": 21, 30 | "parent": "T2" 31 | }, 32 | { 33 | "id": "T5", 34 | "name": "Effect1", 35 | "begin": 22, 36 | "end": 44, 37 | "successor": null, 38 | "children": [ 39 | "T6", 40 | "T7" 41 | ] 42 | }, 43 | { 44 | "id": "T6", 45 | "name": "Variable", 46 | "begin": 22, 47 | "end": 29, 48 | "parent": "T5" 49 | }, 50 | { 51 | "id": "T7", 52 | "name": "Condition", 53 | "begin": 30, 54 | "end": 44, 55 | "parent": "T5" 56 | } 57 | ], 58 | "graph": { 59 | "nodes": [ 60 | { 61 | "id": "N1", 62 | "variable": "a window", 63 | "condition": "opening" 64 | }, 65 | { 66 | "id": "N2", 67 | "variable": "a sound", 68 | "condition": "will be played" 69 | } 70 | ], 71 | "root": "N1", 72 | "edges": [ 73 | { 74 | "origin": "N1", 75 | "target": "N2", 76 | "negated": false 77 | } 78 | ] 79 | }, 80 | "testsuite": { 81 | "conditions": [ 82 | { 83 | "id": "P1", 84 | "variable": "a window", 85 | "condition": "opening" 86 | } 87 | ], 88 | "expected": [ 89 | { 90 | "id": "P2", 91 | "variable": "a sound", 92 | "condition": "will be played" 93 | } 94 | ], 95 | "cases": [ 96 | { 97 | "P1": true, 98 | "P2": true 99 | }, 100 | { 101 | "P1": false, 102 | "P2": false 103 | } 104 | ] 105 | } 106 | } -------------------------------------------------------------------------------- /static/sentences/sentence-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentence": "When the red button is pushed the system shuts down.", 3 | "labels": [ 4 | { 5 | "id": "T2", 6 | "name": "Cause1", 7 | "begin": 5, 8 | "end": 29, 9 | "successor": { 10 | "id": "T5", 11 | "junctor": null 12 | }, 13 | "children": [ 14 | "T3", 15 | "T4" 16 | ] 17 | }, 18 | { 19 | "id": "T3", 20 | "name": "Variable", 21 | "begin": 5, 22 | "end": 19, 23 | "parent": "T2" 24 | }, 25 | { 26 | "id": "T4", 27 | "name": "Condition", 28 | "begin": 20, 29 | "end": 29, 30 | "parent": "T2" 31 | }, 32 | { 33 | "id": "T5", 34 | "name": "Effect1", 35 | "begin": 30, 36 | "end": 51, 37 | "successor": null, 38 | "children": [ 39 | "T6", 40 | "T7" 41 | ] 42 | }, 43 | { 44 | "id": "T6", 45 | "name": "Variable", 46 | "begin": 30, 47 | "end": 40, 48 | "parent": "T5" 49 | }, 50 | { 51 | "id": "T7", 52 | "name": "Condition", 53 | "begin": 41, 54 | "end": 51, 55 | "parent": "T5" 56 | } 57 | ], 58 | "graph": { 59 | "nodes": [ 60 | { 61 | "id": "N1", 62 | "variable": "the red button", 63 | "condition": "is pushed" 64 | }, 65 | { 66 | "id": "N2", 67 | "variable": "the system", 68 | "condition": "shuts down" 69 | } 70 | ], 71 | "root": "N1", 72 | "edges": [ 73 | { 74 | "origin": "N1", 75 | "target": "N2", 76 | "negated": false 77 | } 78 | ] 79 | }, 80 | "testsuite": { 81 | "conditions": [ 82 | { 83 | "id": "P1", 84 | "variable": "the red button", 85 | "condition": "is pushed" 86 | } 87 | ], 88 | "expected": [ 89 | { 90 | "id": "P2", 91 | "variable": "the system", 92 | "condition": "shuts down" 93 | } 94 | ], 95 | "cases": [ 96 | { 97 | "P1": true, 98 | "P2": true 99 | }, 100 | { 101 | "P1": false, 102 | "P2": false 103 | } 104 | ] 105 | } 106 | } -------------------------------------------------------------------------------- /src/converters/sentencetolabels/labeler.py: -------------------------------------------------------------------------------- 1 | import torch 2 | from transformers import BatchEncoding, RobertaTokenizerFast 3 | 4 | import src.converters.sentencetolabels.labelingconverter as lconv 5 | import src.util.constants as consts 6 | from src.converters.sentencetolabels.model import MultiLabelRoBERTaCustomModel 7 | from src.data.labels import Label 8 | 9 | MODEL_TO_USE = 'roberta-base' 10 | LABELER_TO_USE = 'bin/multilabel.ckpt' 11 | DROPOUT_RATE = 0.13780087432114646 12 | 13 | class Labeler: 14 | 15 | def __init__(self, model_path: str=LABELER_TO_USE, useGPU: bool=False, max_len: int=80, dropout: float=DROPOUT_RATE): 16 | # set variables 17 | self.useGPU = useGPU 18 | self.max_len = max_len 19 | 20 | # setup model and tokenizer 21 | self.tokenizer = RobertaTokenizerFast.from_pretrained(MODEL_TO_USE) 22 | self.model = MultiLabelRoBERTaCustomModel.load_from_checkpoint( 23 | hyperparams={'dropout': dropout}, 24 | training_dataset=None, 25 | validation_dataset=None, 26 | test_dataset=None, 27 | labels=consts.LABEL_IDS, 28 | model_to_use=MODEL_TO_USE, 29 | checkpoint_path=model_path 30 | ) 31 | 32 | if self.useGPU: 33 | self.model.cuda() 34 | 35 | self.model.eval() 36 | 37 | def label(self, sentence: str) -> list[Label]: 38 | """Label a given sentence with the available label list. 39 | 40 | parameters: 41 | sentence -- Natural language, english sentence that contains a causal relationship. 42 | 43 | returns: list of labels assigned to that sentence""" 44 | # tokenize the sentence 45 | tokenized_batch: BatchEncoding = self.tokenizer( 46 | text=[sentence], 47 | add_special_tokens=True, 48 | max_length=self.max_len, 49 | truncation=True, 50 | padding='max_length') 51 | 52 | # attention mask 53 | input_ids = torch.tensor(tokenized_batch.input_ids, dtype=torch.long) 54 | attention_mask = torch.tensor(tokenized_batch.attention_mask, dtype=torch.long) 55 | 56 | # utilize CUDA if possible 57 | if self.useGPU: 58 | input_ids = input_ids.cuda() 59 | attention_mask = attention_mask.cuda() 60 | 61 | # generate outputs 62 | outputs = self.model(input_ids, attention_mask, token_type_ids=None, labels=None ) 63 | 64 | # generate prediction 65 | logits = outputs.logits 66 | sigmoid_outputs = torch.sigmoid(logits) 67 | predictions = (sigmoid_outputs >= 0.5).int() 68 | predictions = predictions.cpu() 69 | 70 | # return list of labels 71 | labels: list[Label] = lconv.convert( 72 | sentence_tokens=tokenized_batch[0].tokens, 73 | sentence=sentence, 74 | predictions=predictions) 75 | return labels 76 | 77 | -------------------------------------------------------------------------------- /test/data/graph/IntermediateNode/test_node_condense.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.graph import IntermediateNode, EventNode 4 | 5 | @pytest.mark.integration 6 | def test_pure_conjunction(): 7 | c1 = EventNode(id='E1') 8 | c1.variable = 'var1' 9 | c2 = EventNode(id='E2') 10 | c2.variable = 'var2' 11 | c3 = EventNode(id='E3') 12 | c3.variable = 'var3' 13 | 14 | i1 = IntermediateNode(id='I1', conjunction=True) 15 | i1.add_incoming(c1) 16 | i1.add_incoming(c2) 17 | i2 = IntermediateNode(id='I2', conjunction=True) 18 | i2.add_incoming(c2) 19 | i2.add_incoming(c3) 20 | 21 | assert len(c2.outgoing) == 2 22 | c2.condense() 23 | assert len(c2.outgoing) == 1 24 | 25 | root: IntermediateNode = c3.get_root() 26 | assert root == i1 27 | assert root.conjunction == True 28 | 29 | @pytest.mark.integration 30 | def test_pure_disjunction(): 31 | c1 = EventNode(id='E1') 32 | c1.variable = 'var1' 33 | c2 = EventNode(id='E2') 34 | c2.variable = 'var2' 35 | c3 = EventNode(id='E3') 36 | c3.variable = 'var3' 37 | 38 | i1 = IntermediateNode(id='I1', conjunction=False) 39 | i1.add_incoming(c1) 40 | i1.add_incoming(c2) 41 | i2 = IntermediateNode(id='I2', conjunction=False) 42 | i2.add_incoming(c2) 43 | i2.add_incoming(c3) 44 | 45 | c2.condense() 46 | 47 | root = c3.get_root() 48 | assert root == i1 49 | assert root.conjunction == False 50 | 51 | @pytest.mark.integration 52 | def test_mix(): 53 | c1 = EventNode(id='E1') 54 | c1.variable = 'var1' 55 | c2 = EventNode(id='E2') 56 | c2.variable = 'var2' 57 | c3 = EventNode(id='E3') 58 | c3.variable = 'var3' 59 | 60 | i1 = IntermediateNode(id='I1', conjunction=False) 61 | i1.add_incoming(c1) 62 | i1.add_incoming(c2) 63 | i2 = IntermediateNode(id='I2', conjunction=True) 64 | i2.add_incoming(c2) 65 | i2.add_incoming(c3) 66 | 67 | c2.condense() 68 | root: IntermediateNode = c2.get_root() 69 | assert root.conjunction == False 70 | 71 | child_junctor: IntermediateNode = [inc.origin for inc in root.incoming if type(inc.origin)==IntermediateNode][0] 72 | assert child_junctor.conjunction == True 73 | 74 | @pytest.mark.integration 75 | def test_mix_overruled_precedence(): 76 | c1 = EventNode(id='E1') 77 | c1.variable = 'var1' 78 | c2 = EventNode(id='E2') 79 | c2.variable = 'var2' 80 | c3 = EventNode(id='E3') 81 | c3.variable = 'var3' 82 | 83 | i1 = IntermediateNode(id='I1', conjunction=False, precedence=True) 84 | i1.add_incoming(c1) 85 | i1.add_incoming(c2) 86 | i2 = IntermediateNode(id='I2', conjunction=True) 87 | i2.add_incoming(c2) 88 | i2.add_incoming(c3) 89 | 90 | c2.condense() 91 | root: IntermediateNode = c2.get_root() 92 | assert root.conjunction == True 93 | 94 | child_junctor: IntermediateNode = [inc.origin for inc in root.incoming if type(inc.origin)==IntermediateNode][0] 95 | assert child_junctor.conjunction == False -------------------------------------------------------------------------------- /static/sentences/sentence-16.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentence": "Users which are older than 18 years, are allowed to drive.", 3 | "labels": [ 4 | { 5 | "id": "T1", 6 | "name": "Cause1", 7 | "begin": 0, 8 | "end": 5, 9 | "successor": { 10 | "id": "T3", 11 | "junctor": null 12 | }, 13 | "children": [ 14 | "T2" 15 | ] 16 | }, 17 | { 18 | "id": "T2", 19 | "name": "Variable", 20 | "begin": 0, 21 | "end": 5, 22 | "parent": "T1" 23 | }, 24 | { 25 | "id": "T3", 26 | "name": "Cause1", 27 | "begin": 12, 28 | "end": 35, 29 | "successor": { 30 | "id": "T5", 31 | "junctor": null 32 | }, 33 | "children": [ 34 | "T4" 35 | ] 36 | }, 37 | { 38 | "id": "T4", 39 | "name": "Condition", 40 | "begin": 12, 41 | "end": 35, 42 | "parent": "T3" 43 | }, 44 | { 45 | "id": "T5", 46 | "name": "Effect1", 47 | "begin": 37, 48 | "end": 57, 49 | "successor": null, 50 | "children": [ 51 | "T6" 52 | ] 53 | }, 54 | { 55 | "id": "T6", 56 | "name": "Condition", 57 | "begin": 37, 58 | "end": 57, 59 | "parent": "T5" 60 | } 61 | ], 62 | "graph": { 63 | "nodes": [ 64 | { 65 | "id": "N1", 66 | "variable": "Users", 67 | "condition": "are older than 18 years" 68 | }, 69 | { 70 | "id": "N2", 71 | "variable": "Users", 72 | "condition": "are allowed to drive" 73 | } 74 | ], 75 | "root": "N1", 76 | "edges": [ 77 | { 78 | "origin": "N1", 79 | "target": "N2", 80 | "negated": false 81 | } 82 | ] 83 | }, 84 | "testsuite": { 85 | "conditions": [ 86 | { 87 | "id": "P1", 88 | "variable": "Users", 89 | "condition": "are older than 18 years" 90 | } 91 | ], 92 | "expected": [ 93 | { 94 | "id": "P2", 95 | "variable": "Users", 96 | "condition": "are allowed to drive" 97 | } 98 | ], 99 | "cases": [ 100 | { 101 | "P1": true, 102 | "P2": true 103 | }, 104 | { 105 | "P1": false, 106 | "P2": false 107 | } 108 | ] 109 | } 110 | } -------------------------------------------------------------------------------- /static/sentences/sentence-1b.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentence": "When the green button is pushed the system does not shut down.", 3 | "labels": [ 4 | { 5 | "id": "T2", 6 | "name": "Cause1", 7 | "begin": 5, 8 | "end": 31, 9 | "successor": { 10 | "id": "T5", 11 | "junctor": null 12 | }, 13 | "children": [ 14 | "T3", 15 | "T4" 16 | ] 17 | }, 18 | { 19 | "id": "T3", 20 | "name": "Variable", 21 | "begin": 5, 22 | "end": 21, 23 | "parent": "T2" 24 | }, 25 | { 26 | "id": "T4", 27 | "name": "Condition", 28 | "begin": 22, 29 | "end": 31, 30 | "parent": "T2" 31 | }, 32 | { 33 | "id": "T5", 34 | "name": "Effect1", 35 | "begin": 32, 36 | "end": 61, 37 | "successor": null, 38 | "children": [ 39 | "T6", 40 | "T7", 41 | "T8" 42 | ] 43 | }, 44 | { 45 | "id": "T6", 46 | "name": "Variable", 47 | "begin": 32, 48 | "end": 42, 49 | "parent": "T5" 50 | }, 51 | { 52 | "id": "T7", 53 | "name": "Negation", 54 | "begin": 43, 55 | "end": 51, 56 | "parent": "T5" 57 | }, 58 | { 59 | "id": "T8", 60 | "name": "Condition", 61 | "begin": 52, 62 | "end": 61, 63 | "parent": "T5" 64 | } 65 | ], 66 | "graph": { 67 | "nodes": [ 68 | { 69 | "id": "N1", 70 | "variable": "the green button", 71 | "condition": "is pushed" 72 | }, 73 | { 74 | "id": "N2", 75 | "variable": "the system", 76 | "condition": "shut down" 77 | } 78 | ], 79 | "root": "N1", 80 | "edges": [ 81 | { 82 | "origin": "N1", 83 | "target": "N2", 84 | "negated": true 85 | } 86 | ] 87 | }, 88 | "testsuite": { 89 | "conditions": [ 90 | { 91 | "id": "P1", 92 | "variable": "the green button", 93 | "condition": "is pushed" 94 | } 95 | ], 96 | "expected": [ 97 | { 98 | "id": "P2", 99 | "variable": "the system", 100 | "condition": "shut down" 101 | } 102 | ], 103 | "cases": [ 104 | { 105 | "P1": true, 106 | "P2": false 107 | }, 108 | { 109 | "P1": false, 110 | "P2": true 111 | } 112 | ] 113 | } 114 | } -------------------------------------------------------------------------------- /src/converters/sentencetolabels/model.py: -------------------------------------------------------------------------------- 1 | import abc 2 | 3 | import pytorch_lightning as pl 4 | import torch 5 | from torch import nn 6 | from torch.nn import BCEWithLogitsLoss 7 | from transformers import RobertaModel 8 | from transformers.modeling_outputs import TokenClassifierOutput 9 | 10 | 11 | class CustomModel(pl.LightningModule): 12 | 13 | def __init__(self, hyperparams, training_dataset, validation_dataset, test_dataset, labels, model_to_use): 14 | super().__init__() 15 | self.hyperparams = hyperparams 16 | self.training_dataset = training_dataset 17 | self.validation_dataset = validation_dataset 18 | self.test_dataset = test_dataset 19 | self.labels = labels 20 | self.label2idx = {t: i for i, t in enumerate(labels)} 21 | self.define_model(model_to_use, len(labels)) 22 | 23 | # Variable used to keep track of the best result obtained 24 | self.best_score = 0 25 | self.epoch_best_score = 0 26 | 27 | @abc.abstractmethod 28 | def define_model(self, model_to_use, num_labels): 29 | raise NotImplementedError 30 | 31 | @abc.abstractmethod 32 | def forward(self, input_ids, attention_mask, token_type_ids, targets): 33 | raise NotImplementedError 34 | 35 | @abc.abstractmethod 36 | def get_predictions_from_logits(self, logits): 37 | raise NotImplementedError 38 | 39 | 40 | class MultiLabelRoBERTaCustomModel(CustomModel): 41 | 42 | def get_predictions_from_logits(self, logits): 43 | sigmoid_outputs = torch.sigmoid(logits) 44 | predictions = (sigmoid_outputs >= 0.5).int() 45 | 46 | return predictions 47 | 48 | def define_model(self, model_to_use, num_labels): 49 | self.num_labels = num_labels 50 | self.bert = RobertaModel.from_pretrained(model_to_use) 51 | self.dropout = nn.Dropout(self.hyperparams["dropout"]) 52 | self.classifier = nn.Linear( 53 | self.bert.config.hidden_size, self.num_labels) 54 | 55 | def forward(self, input_ids, attention_mask, token_type_ids, labels): 56 | outputs = self.bert( 57 | input_ids=input_ids, 58 | attention_mask=attention_mask 59 | ) 60 | 61 | sequence_output = outputs[0] 62 | 63 | sequence_output = self.dropout(sequence_output) 64 | logits = self.classifier(sequence_output) 65 | 66 | loss = None 67 | if labels is not None: 68 | loss_fct = BCEWithLogitsLoss() 69 | # Only keep active parts of the loss 70 | if attention_mask is not None: 71 | active_logits = logits[attention_mask == 1] 72 | 73 | active_labels = labels[attention_mask == 74 | 1].type_as(active_logits) 75 | 76 | loss = loss_fct(active_logits, active_labels) 77 | else: 78 | loss = loss_fct( 79 | logits.view(-1, self.num_labels), labels.view(-1)) 80 | 81 | return TokenClassifierOutput( 82 | loss=loss, 83 | logits=logits, 84 | hidden_states=outputs.hidden_states, 85 | attentions=outputs.attentions, 86 | ) 87 | -------------------------------------------------------------------------------- /static/sentences/sentence-1c.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentence": "When the red button is not pushed the system does not shut down.", 3 | "labels": [ 4 | { 5 | "id": "T2", 6 | "name": "Cause1", 7 | "begin": 5, 8 | "end": 33, 9 | "successor": { 10 | "id": "T6", 11 | "junctor": null 12 | }, 13 | "children": [ 14 | "T3", 15 | "T4", 16 | "T5" 17 | ] 18 | }, 19 | { 20 | "id": "T3", 21 | "name": "Variable", 22 | "begin": 5, 23 | "end": 19, 24 | "parent": "T2" 25 | }, 26 | { 27 | "id": "T4", 28 | "name": "Negation", 29 | "begin": 20, 30 | "end": 26, 31 | "parent": "T2" 32 | }, 33 | { 34 | "id": "T5", 35 | "name": "Condition", 36 | "begin": 27, 37 | "end": 33, 38 | "parent": "T2" 39 | }, 40 | { 41 | "id": "T6", 42 | "name": "Effect1", 43 | "begin": 34, 44 | "end": 63, 45 | "successor": null, 46 | "children": [ 47 | "T7", 48 | "T8", 49 | "T9" 50 | ] 51 | }, 52 | { 53 | "id": "T7", 54 | "name": "Variable", 55 | "begin": 34, 56 | "end": 44, 57 | "parent": "T6" 58 | }, 59 | { 60 | "id": "T8", 61 | "name": "Negation", 62 | "begin": 45, 63 | "end": 53, 64 | "parent": "T6" 65 | }, 66 | { 67 | "id": "T9", 68 | "name": "Condition", 69 | "begin": 54, 70 | "end": 63, 71 | "parent": "T6" 72 | } 73 | ], 74 | "graph": { 75 | "nodes": [ 76 | { 77 | "id": "N1", 78 | "variable": "the red button", 79 | "condition": "pushed" 80 | }, 81 | { 82 | "id": "N2", 83 | "variable": "the system", 84 | "condition": "shut down" 85 | } 86 | ], 87 | "root": "N1", 88 | "edges": [ 89 | { 90 | "origin": "N1", 91 | "target": "N2", 92 | "negated": false 93 | } 94 | ] 95 | }, 96 | "testsuite": { 97 | "conditions": [ 98 | { 99 | "id": "P1", 100 | "variable": "the red button", 101 | "condition": "pushed" 102 | } 103 | ], 104 | "expected": [ 105 | { 106 | "id": "P2", 107 | "variable": "the system", 108 | "condition": "shut down" 109 | } 110 | ], 111 | "cases": [ 112 | { 113 | "P1": true, 114 | "P2": true 115 | }, 116 | { 117 | "P1": false, 118 | "P2": false 119 | } 120 | ] 121 | } 122 | } -------------------------------------------------------------------------------- /test/converters/labelstograph/eventconnector/test_junctor_map.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.converters.labelstograph.eventconnector import get_junctors 4 | 5 | from src.data.graph import EventNode 6 | from src.data.labels import EventLabel 7 | 8 | @pytest.mark.unit 9 | def test_conjunction(): 10 | c1 = EventLabel(id='L1', name='Cause1', begin=0, end=10) 11 | c2 = EventLabel(id='L2', name='Cause2', begin=15, end=20) 12 | c1.set_successor(successor=c2, junctor='AND') 13 | 14 | nodes = [EventNode(id='E1', labels=[c1]), EventNode(id='E2', labels=[c2])] 15 | 16 | junctors = get_junctors(events=nodes) 17 | assert junctors[('E1', 'E2')] == 'AND' 18 | 19 | @pytest.mark.unit 20 | def test_disjunction(): 21 | c1 = EventLabel(id='L1', name='Cause1', begin=0, end=10) 22 | c2 = EventLabel(id='L2', name='Cause2', begin=15, end=20) 23 | c1.set_successor(successor=c2, junctor='OR') 24 | 25 | nodes = [EventNode(id='E1', labels=[c1]), EventNode(id='E2', labels=[c2])] 26 | 27 | junctors = get_junctors(events=nodes) 28 | assert junctors[('E1', 'E2')] == 'OR' 29 | 30 | @pytest.mark.unit 31 | def test_nojunction(): 32 | c1 = EventLabel(id='L1', name='Cause1', begin=0, end=10) 33 | c2 = EventLabel(id='L2', name='Cause2', begin=15, end=20) 34 | c1.set_successor(successor=c2, junctor=None) 35 | 36 | nodes = [EventNode(id='E1', labels=[c1]), EventNode(id='E2', labels=[c2])] 37 | 38 | # if no junctor is available at all, assume a conjunction 39 | junctors = get_junctors(events=nodes) 40 | assert junctors[('E1', 'E2')] == 'AND' 41 | 42 | @pytest.mark.unit 43 | def test_implicit_conjunction(): 44 | c1 = EventLabel(id='L1', name='Cause1', begin=0, end=10) 45 | c2 = EventLabel(id='L2', name='Cause2', begin=15, end=20) 46 | c3 = EventLabel(id='L3', name='Cause3', begin=25, end=30) 47 | c1.set_successor(successor=c2, junctor=None) 48 | c2.set_successor(successor=c3, junctor='AND') 49 | 50 | nodes = [EventNode(id='E1', labels=[c1]), EventNode(id='E2', labels=[c2]), EventNode(id='E3', labels=[c3])] 51 | 52 | junctors = get_junctors(events=nodes) 53 | assert junctors[('E1', 'E2')] == 'AND' 54 | 55 | @pytest.mark.unit 56 | def test_overruled_precedence(): 57 | c1 = EventLabel(id='L1', name='Cause1', begin=0, end=10) 58 | c2 = EventLabel(id='L2', name='Cause2', begin=15, end=20) 59 | c3 = EventLabel(id='L3', name='Cause3', begin=25, end=30) 60 | c1.set_successor(successor=c2, junctor='AND') 61 | c2.set_successor(successor=c3, junctor='POR') 62 | 63 | nodes = [ 64 | EventNode(id='E1', labels=[c1]), 65 | EventNode(id='E2', labels=[c2]), 66 | EventNode(id='E3', labels=[c3])] 67 | 68 | junctors = get_junctors(events=nodes) 69 | assert junctors[('E2', 'E3')] == 'POR' 70 | 71 | @pytest.mark.unit 72 | def test_causes_after_events(): 73 | e1 = EventLabel(id='L1', name='Effect1', begin=0, end = 10) 74 | c1 = EventLabel(id='L2', name='Cause1', begin=15, end=20) 75 | c2 = EventLabel(id='L3', name='Cause2', begin=25, end=30) 76 | e1.set_successor(successor=c1, junctor=None) 77 | c1.set_successor(successor=c2, junctor='OR') 78 | 79 | causes = [ 80 | EventNode(id='E2', labels=[c1]), 81 | EventNode(id='E3', labels=[c2])] 82 | 83 | junctors = get_junctors(events=causes) 84 | assert junctors[('E2', 'E3')] == 'OR' -------------------------------------------------------------------------------- /test/converters/labelstograph/eventconnector/test_generate_initial_nodenet.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import patch 3 | 4 | from src.converters.labelstograph.eventconnector import generate_initial_nodenet as gin 5 | 6 | from src.data.graph import EventNode 7 | 8 | @pytest.mark.unit 9 | @patch('src.converters.labelstograph.eventconnector.EventNode.is_negated') 10 | def test_two_conjunction(mock_isnegated): 11 | mock_isnegated.return_value == False 12 | events = [EventNode(id='E1'), EventNode(id='E2')] 13 | jm = {('E1', 'E2'): 'AND'} 14 | 15 | intermediates, _ = gin(events=events, junctor_map=jm) 16 | 17 | assert len(intermediates) == 1 18 | assert len(intermediates[0].incoming) == 2 19 | assert intermediates[0].incoming[0].origin == events[0] 20 | assert intermediates[0].incoming[1].origin == events[1] 21 | 22 | @pytest.mark.unit 23 | @patch('src.converters.labelstograph.eventconnector.EventNode.is_negated') 24 | def test_two_negation(mock_isnegated): 25 | mock_isnegated.return_value == True 26 | events: list[EventNode] = [EventNode(id='E1'), EventNode(id='E2')] 27 | jm = {('E1', 'E2'): 'AND'} 28 | 29 | intermediates, _ = gin(events=events, junctor_map=jm) 30 | print(intermediates[0].incoming) 31 | 32 | assert intermediates[0].incoming[0].negated == True 33 | assert intermediates[0].incoming[1].negated == True 34 | 35 | @pytest.mark.unit 36 | @patch('src.converters.labelstograph.eventconnector.EventNode.is_negated') 37 | def test_three_conj_disj(mock_isnegated): 38 | mock_isnegated.return_value == False 39 | events = [EventNode(id='E1'), EventNode(id='E2'), EventNode(id='E3')] 40 | jm = {('E1', 'E2'): 'AND', ('E2', 'E3'): 'OR'} 41 | 42 | intermediates, _ = gin(events=events, junctor_map=jm) 43 | 44 | assert len(intermediates) == 2 45 | assert len(intermediates[0].incoming) == 2 46 | assert intermediates[0].conjunction == True 47 | assert intermediates[0].precedence == False 48 | assert len(intermediates[1].incoming) == 2 49 | assert intermediates[1].conjunction == False 50 | assert intermediates[1].precedence == False 51 | assert intermediates[0].incoming[0].origin == events[0] 52 | assert intermediates[0].incoming[1].origin == events[1] 53 | assert intermediates[1].incoming[0].origin == events[1] 54 | assert intermediates[1].incoming[1].origin == events[2] 55 | 56 | @pytest.mark.unit 57 | @patch('src.converters.labelstograph.eventconnector.EventNode.is_negated') 58 | def test_three_conj_disj_overruled_precedence(mock_isnegated): 59 | mock_isnegated.return_value == False 60 | 61 | events = [EventNode(id='E1'), EventNode(id='E2'), EventNode(id='E3')] 62 | jm = {('E1', 'E2'): 'AND', ('E2', 'E3'): 'POR'} 63 | 64 | intermediates, _ = gin(events=events, junctor_map=jm) 65 | 66 | assert len(intermediates) == 2 67 | assert len(intermediates[0].incoming) == 2 68 | assert intermediates[0].conjunction == True 69 | assert intermediates[0].precedence == False 70 | assert len(intermediates[1].incoming) == 2 71 | assert intermediates[1].conjunction == False 72 | assert intermediates[1].precedence == True 73 | assert intermediates[0].incoming[0].origin == events[0] 74 | assert intermediates[0].incoming[1].origin == events[1] 75 | assert intermediates[1].incoming[0].origin == events[1] 76 | assert intermediates[1].incoming[1].origin == events[2] -------------------------------------------------------------------------------- /static/sentences/sentence-18.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentence": "Before access permissions are deactivated, I receive information as to which app functions will no longer work (in full).", 3 | "labels": [ 4 | { 5 | "id": "T1", 6 | "name": "Cause1", 7 | "begin": 7, 8 | "end": 41, 9 | "successor": { 10 | "id": "T5", 11 | "junctor": null 12 | }, 13 | "children": [ 14 | "T2", 15 | "T3" 16 | ] 17 | }, 18 | { 19 | "id": "T2", 20 | "name": "Variable", 21 | "begin": 7, 22 | "end": 25, 23 | "parent": "T1" 24 | }, 25 | { 26 | "id": "T3", 27 | "name": "Condition", 28 | "begin": 26, 29 | "end": 41, 30 | "parent": "T2" 31 | }, 32 | { 33 | "id": "T5", 34 | "name": "Effect1", 35 | "begin": 43, 36 | "end": 110, 37 | "successor": null, 38 | "children": [ 39 | "T6", 40 | "T7", 41 | "T8", 42 | "T9", 43 | "T10" 44 | ] 45 | }, 46 | { 47 | "id": "T6", 48 | "name": "Variable", 49 | "begin": 43, 50 | "end": 44, 51 | "parent": "T5" 52 | }, 53 | { 54 | "id": "T7", 55 | "name": "Condition", 56 | "begin": 45, 57 | "end": 76, 58 | "parent": "T5" 59 | }, 60 | { 61 | "id": "T8", 62 | "name": "Variable", 63 | "begin": 77, 64 | "end": 90, 65 | "parent": "T5" 66 | }, 67 | { 68 | "id": "T9", 69 | "name": "Negation", 70 | "begin": 91, 71 | "end": 98, 72 | "parent": "T5" 73 | }, 74 | { 75 | "id": "T10", 76 | "name": "Condition", 77 | "begin": 99, 78 | "end": 110, 79 | "parent": "T5" 80 | } 81 | ], 82 | "graph": { 83 | "nodes": [ 84 | { 85 | "id": "N1", 86 | "variable": "access permissions", 87 | "condition": "are deactivated" 88 | }, 89 | { 90 | "id": "N2", 91 | "variable": "I app functions", 92 | "condition": "receive information as to which longer work" 93 | } 94 | ], 95 | "root": "N1", 96 | "edges": [ 97 | { 98 | "origin": "N1", 99 | "target": "N2", 100 | "negated": true 101 | } 102 | ] 103 | }, 104 | "testsuite": { 105 | "conditions": [ 106 | { 107 | "id": "P1", 108 | "variable": "access permissions", 109 | "condition": "are deactivated" 110 | } 111 | ], 112 | "expected": [ 113 | { 114 | "id": "P2", 115 | "variable": "I app functions", 116 | "condition": "receive information as to which longer work" 117 | } 118 | ], 119 | "cases": [ 120 | { 121 | "P1": true, 122 | "P2": false 123 | }, 124 | { 125 | "P1": false, 126 | "P2": true 127 | } 128 | ] 129 | } 130 | } -------------------------------------------------------------------------------- /test/converters/labelstograph/eventconnector/test_eventconnector.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from unittest.mock import patch 3 | 4 | from src.converters.labelstograph.eventconnector import connect_events 5 | 6 | from src.data.labels import EventLabel, SubLabel 7 | from src.data.graph import EventNode, IntermediateNode 8 | 9 | @pytest.mark.integration 10 | def test_connection(): 11 | sentence = 'If an error is present or the debugger is active and an exception is triggered' 12 | 13 | cause1 = EventLabel(id="T2", name="Cause1", begin=3, end=22) 14 | cause1var = SubLabel(id="T3", name="Variable", begin=3, end=11) 15 | cause1cond = SubLabel(id="T4", name="Condition", begin=12, end=22) 16 | cause1.add_child(cause1var) 17 | cause1.add_child(cause1cond) 18 | 19 | disj = SubLabel(id="T5", name="Disjunction", begin=23, end=25) 20 | 21 | cause2 = EventLabel(id="T6", name="Cause2", begin=26, end=48) 22 | cause2var = SubLabel(id="T7", name="Variable", begin=26, end=38) 23 | cause2cond = SubLabel(id="T8", name="Condition", begin=39, end=48) 24 | cause2.add_child(cause2var) 25 | cause2.add_child(cause2cond) 26 | cause1.set_successor(cause2, junctor='OR') 27 | 28 | conj = SubLabel(id="T9", name="Conjunction", begin=49, end=52) 29 | 30 | cause3 = EventLabel(id="T10", name="Cause3", begin=53, end=78) 31 | cause3var = SubLabel(id="T11", name="Variable", begin=53, end=65) 32 | cause3cond = SubLabel(id="T12", name="Condition", begin=66, end=78) 33 | cause3.add_child(cause3var) 34 | cause3.add_child(cause3cond) 35 | cause2.set_successor(cause3, junctor='AND') 36 | 37 | # Events 38 | event1 = EventNode(id='E1', labels=[cause1]) 39 | event1.variable = 'an error' 40 | event1.condition = 'is present' 41 | event2 = EventNode(id='E2', labels=[cause2]) 42 | event2.variable = 'the debugger' 43 | event2.condition = 'is active' 44 | event3 = EventNode(id='E3', labels=[cause3]) 45 | event3.variable = 'an exception' 46 | event3.condition = 'is triggered' 47 | 48 | events = [event1, event2, event3] 49 | 50 | causes, _ = connect_events(events) 51 | 52 | # there should be 5 cause nodes: 3 event nodes + 2 intermediate nodes 53 | assert len(causes) == 5 54 | 55 | root: IntermediateNode = causes[0] 56 | assert root.conjunction == False 57 | 58 | @pytest.mark.integration 59 | @patch('src.converters.labelstograph.eventconnector.generate_initial_nodenet') 60 | @patch('src.converters.labelstograph.eventconnector.get_junctors') 61 | def test_connection_overruled_precedence(mock_gj, mock_gin): 62 | """Test the event connection in a scenario with overruled precedence (e.g., "If A and either B or C" which should resolve to (A && (B || C)). The root node should be the conjunction, because the precedence of the disjunction overrules the conjunction in this case.""" 63 | 64 | cause1 = EventNode(id='c1', labels=None, variable="v1") 65 | cause2 = EventNode(id='c2', labels=None, variable="v2") 66 | cause3 = EventNode(id='c3', labels=None, variable="v3") 67 | 68 | edgelist = [] 69 | i1 = IntermediateNode(id='i1', conjunction=True) 70 | edgelist.append(i1.add_incoming(cause1)) 71 | edgelist.append(i1.add_incoming(cause2)) 72 | i2 = IntermediateNode(id='i2', conjunction=False, precedence=True) 73 | edgelist.append(i2.add_incoming(cause2)) 74 | edgelist.append(i2.add_incoming(cause3)) 75 | 76 | mock_gj.return_value = None 77 | mock_gin.return_value = ([i1, i2], edgelist) 78 | 79 | nodes, edges = connect_events([cause1, cause2, cause3]) 80 | root = nodes[0] 81 | 82 | # make sure the root node is an intermediate node 83 | assert type(root) == IntermediateNode 84 | root: IntermediateNode = root 85 | 86 | # assert that this intermediate node is the conjunction, not the disjunction 87 | assert root.conjunction == True 88 | -------------------------------------------------------------------------------- /test/converters/labelstograph/graphconverter/test_resolve_exceptive_negations.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.converters.labelstograph.graphconverter import resolve_exceptive_negations 4 | 5 | from src.data.labels import EventLabel, SubLabel 6 | 7 | @pytest.mark.integration 8 | def test_exceptive_negation_found(): 9 | labels = [ 10 | SubLabel(id='L1', name='Negation', begin=0, end=6), 11 | EventLabel(id='L2', name='Cause1', begin=7, end=17) 12 | ] 13 | 14 | affected_labels = resolve_exceptive_negations(labels) 15 | 16 | expected = [labels[1]] 17 | assert affected_labels == expected 18 | 19 | @pytest.mark.integration 20 | def test_exceptive_negation_found_cascading(): 21 | labels = [ 22 | SubLabel(id='L1', name='Negation', begin=0, end=6), 23 | EventLabel(id='L2', name='Cause1', begin=7, end=17), 24 | EventLabel(id='L3', name='Cause2', begin=18, end=28) 25 | ] 26 | c1: EventLabel = labels[1] 27 | c1.set_successor(labels[2], "AND") 28 | 29 | affected_labels = resolve_exceptive_negations(labels) 30 | 31 | expected = [labels[1], labels[2]] 32 | assert affected_labels == expected 33 | 34 | @pytest.mark.integration 35 | def test_exceptive_negation_found_cascading_twice(): 36 | labels = [ 37 | SubLabel(id='L1', name='Negation', begin=0, end=6), 38 | EventLabel(id='L2', name='Cause1', begin=7, end=17), 39 | EventLabel(id='L3', name='Cause2', begin=18, end=28), 40 | EventLabel(id='L4', name='Cause3', begin=29, end=39) 41 | ] 42 | c1: EventLabel = labels[1] 43 | c2: EventLabel = labels[2] 44 | c1.set_successor(c2, "AND") 45 | c2.set_successor(labels[3], "AND") 46 | 47 | affected_labels = resolve_exceptive_negations(labels) 48 | 49 | expected = [labels[1], labels[2], labels[3]] 50 | assert affected_labels == expected 51 | 52 | @pytest.mark.integration 53 | def test_exceptive_negation_found_not_cascading(): 54 | labels = [ 55 | SubLabel(id='L1', name='Negation', begin=0, end=6), 56 | EventLabel(id='L2', name='Cause1', begin=7, end=17), 57 | EventLabel(id='L3', name='Cause2', begin=18, end=28) 58 | ] 59 | c1: EventLabel = labels[1] 60 | c1.set_successor(labels[2], "OR") 61 | 62 | affected_labels = resolve_exceptive_negations(labels) 63 | 64 | expected = [labels[1]] 65 | assert affected_labels == expected 66 | 67 | @pytest.mark.integration 68 | def test_exceptive_negation_found_cascading_stopped(): 69 | labels = [ 70 | SubLabel(id='L1', name='Negation', begin=0, end=6), 71 | EventLabel(id='L2', name='Cause1', begin=7, end=17), 72 | EventLabel(id='L3', name='Cause2', begin=18, end=28), 73 | EventLabel(id='L4', name='Cause3', begin=29, end=39) 74 | ] 75 | c1: EventLabel = labels[1] 76 | c2: EventLabel = labels[2] 77 | c1.set_successor(c2, "AND") 78 | c2.set_successor(labels[3], "OR") 79 | 80 | affected_labels = resolve_exceptive_negations(labels) 81 | 82 | expected = [labels[1], labels[2]] 83 | assert affected_labels == expected 84 | 85 | @pytest.mark.integration 86 | def test_exceptive_negation_found_cascading(): 87 | labels = [ 88 | SubLabel(id='L1', name='Negation', begin=0, end=6), 89 | EventLabel(id='L2', name='Cause1', begin=7, end=17), 90 | EventLabel(id='L3', name='Effect1', begin=18, end=28) 91 | ] 92 | c1: EventLabel = labels[1] 93 | c1.set_successor(labels[2], None) 94 | 95 | affected_labels = resolve_exceptive_negations(labels) 96 | 97 | expected = [labels[1]] 98 | assert affected_labels == expected 99 | 100 | @pytest.mark.integration 101 | def test_no_exceptive_negation_found(): 102 | negation = SubLabel(id='L1', name='Negation', begin=0, end=6) 103 | event = EventLabel(id='L2', name='Cause1', begin=7, end=17) 104 | event.add_child(child=negation) 105 | labels = [negation, event] 106 | 107 | affected_labels = resolve_exceptive_negations(labels) 108 | assert affected_labels == [] -------------------------------------------------------------------------------- /test/api/service/test_api_isolated.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import unittest.mock as mock 3 | from unittest.mock import patch 4 | 5 | from src.api.service import CiRAServiceImpl 6 | 7 | from src.data.labels import SubLabel 8 | from src.data.graph import Graph, EventNode 9 | from src.data.test import Suite, Parameter 10 | 11 | # since the CiRAConverter is mocked in this test suite, define all objects (and their appropriate serialization) in advance 12 | sentence = "If the button is pressed then the system shuts down." 13 | 14 | classification = (True, 0.84) 15 | 16 | labels = [SubLabel(id='L1', name='Variable', begin=10, end=15)] 17 | labels_serialized = [{'id': 'L1', 'name': 'Variable', 18 | 'begin': 10, 'end': 15, 'parent': None}] 19 | 20 | nodes = [EventNode(id='c', variable='the button', condition='is pressed'), EventNode( 21 | id='e', variable='the system', condition='shuts down')] 22 | edge = nodes[1].add_incoming(nodes[0]) 23 | graph = Graph(nodes=nodes, root=nodes[0], edges=[edge]) 24 | graph_serialized = { 25 | 'nodes': [{'id': 'c', 'variable': 'the button', 'condition': 'is pressed'}, {'id': 'e', 'variable': 'the system', 'condition': 'shuts down'}], 26 | 'root': 'c', 27 | 'edges': [{'origin': 'c', 'target': 'e', 'negated': False}] 28 | } 29 | 30 | suite = Suite( 31 | conditions=[Parameter(id='c', variable='the button', 32 | condition='is pressed')], 33 | expected=[Parameter(id='e', variable='the system', 34 | condition='shuts down')], 35 | cases=[{'c': True, 'e': True}, {'c': False, 'e': False}] 36 | ) 37 | suite_serialized = { 38 | 'conditions': [{'id': 'c', 'variable': 'the button', 39 | 'condition': 'is pressed'}], 40 | 'expected': [{'id': 'e', 'variable': 'the system', 41 | 'condition': 'shuts down'}], 42 | 'cases': [{'c': True, 'e': True}, {'c': False, 'e': False}] 43 | } 44 | 45 | 46 | @pytest.fixture(scope="module") 47 | @patch('src.api.service.CiRAConverter', autospec=True) 48 | def isolatedService(converter) -> CiRAServiceImpl: 49 | # mock the CiRAConverter to isolate the system under test from it 50 | mockedConverter = mock.MagicMock() 51 | mockedConverter.classify.return_value = classification 52 | mockedConverter.label.return_value = labels 53 | mockedConverter.graph.return_value = graph 54 | mockedConverter.testsuite.return_value = suite 55 | 56 | # plug the mocked converter into the SUT 57 | converter.return_value = mockedConverter 58 | service = CiRAServiceImpl(model_classification=None, model_labeling=None) 59 | return service 60 | 61 | 62 | @pytest.mark.unit 63 | def test_classify(isolatedService): 64 | classification = isolatedService.classify(sentence) 65 | assert classification == classification 66 | 67 | 68 | @pytest.mark.unit 69 | def test_sentence_to_labels(isolatedService): 70 | labels = isolatedService.sentence_to_labels(sentence) 71 | assert labels == labels_serialized 72 | 73 | 74 | @pytest.mark.unit 75 | def test_sentence_to_graph_unlabeled(isolatedService): 76 | graph = isolatedService.sentence_to_graph(sentence, labels=None) 77 | assert graph == graph_serialized 78 | 79 | 80 | @pytest.mark.unit 81 | def test_sentence_to_graph_original_labels(isolatedService): 82 | graph = isolatedService.sentence_to_graph( 83 | sentence, labels=labels) 84 | assert graph == graph_serialized 85 | 86 | 87 | @pytest.mark.unit 88 | def test_sentence_to_graph_serialized_labels(isolatedService): 89 | graph = isolatedService.sentence_to_graph( 90 | sentence, labels=labels_serialized) 91 | assert graph == graph_serialized 92 | 93 | 94 | @pytest.mark.unit 95 | def test_graph_to_test_from_graph(isolatedService): 96 | generated_suite = isolatedService.graph_to_test(graph=graph, sentence=sentence) 97 | assert generated_suite == suite_serialized 98 | 99 | 100 | @pytest.mark.unit 101 | def test_graph_to_test_from_dictgraph(isolatedService): 102 | generated_suite = isolatedService.graph_to_test(graph=graph_serialized, sentence=sentence) 103 | assert generated_suite == suite_serialized 104 | -------------------------------------------------------------------------------- /src/converters/graphtotestsuite/testsuiteconverter.py: -------------------------------------------------------------------------------- 1 | from src.data.graph import Graph, EventNode 2 | from src.data.test import Suite, Parameter 3 | 4 | def convert(graph: Graph) -> Suite: 5 | """Generate a test suite from a cause-effect graph. The test suite contains one parameter per event node (cause-nodes become input-/condition-parameters, effect-nodes become (expected) outcome parameters) and a minimal list of test cases necessary to evaluate the root cause node both to true and false, effectively covering all relevant, unique configurations of parameters which would assert that a system exhibits the behavior entailed by the cause-effect graph. 6 | 7 | parameters: 8 | graph -- a cause-effect graph representing a causal sentence 9 | 10 | returns: a minimal test suite that describes how to assert that a system exhibits the behavior entailed by the graph.""" 11 | # obtain all event nodes from the graph and generate a mapping from those nodes to parameters 12 | events = [node for node in graph.nodes if type(node) == EventNode] 13 | causeids = [event.id for event in events if len(event.incoming) == 0] 14 | effects = [event for event in events if len(event.outgoing) == 0] 15 | effectids = [event.id for event in events if len(event.outgoing) == 0] 16 | 17 | # generate all parameters from the events 18 | parameters_map: dict = generate_parameters(nodes=events) 19 | input_parameters_map: dict = {node: parameters_map[node] for node in parameters_map if node in causeids} 20 | output_parameters_map: dict = {node: parameters_map[node] for node in parameters_map if node in effectids} 21 | 22 | # generate test cases for the root node being evaluated both to True and False 23 | test_cases = [] 24 | for root_node_outcome in [True, False]: 25 | # determine the expected value of each effect node given the root node outcome 26 | outcome_configuration: dict = get_expected_outcome(root_node_evaluation=root_node_outcome, effects=effects) 27 | expected_outcome = map_node_to_parameter(configuration=outcome_configuration, parameters_map=parameters_map) 28 | 29 | # determine all non-redundant configurations of cause node values able to produce the root node outcome 30 | node_configurations: list[dict] = graph.root.get_testcase_configuration(expected_outcome=root_node_outcome) 31 | for config in node_configurations: 32 | # for each configuration: generate one test case 33 | test_case = map_node_to_parameter(configuration=config, parameters_map = parameters_map) 34 | test_cases.append(test_case | expected_outcome) 35 | 36 | return Suite(conditions=list(input_parameters_map.values()), expected=list(output_parameters_map.values()), cases=test_cases) 37 | 38 | def generate_parameters(nodes: list[EventNode]) -> dict: 39 | """Convert every node in the list into a parameter for a test suite 40 | 41 | parameters: 42 | nodes -- list of event nodes 43 | 44 | returns: a mapping between nodes and generated parameters""" 45 | 46 | return {node.id: Parameter(id=f'P{index}', variable=node.variable, condition=node.condition) for index, node in enumerate(nodes)} 47 | 48 | def get_expected_outcome(root_node_evaluation: bool, effects: list[EventNode]) -> dict: 49 | """Generate a mapping of effect event nodes to an expected value. 50 | 51 | parameters: 52 | root_node_evaluation -- whether the root cause node is supposed to be evaluated to True or false 53 | effects -- list of event nodes that represent the effects in the cause-effect-graph 54 | 55 | returns: mapping from each effect to the expected value given the root node evaluation""" 56 | 57 | return {effect.id: (effect.incoming[0].negated != root_node_evaluation) for effect in effects} 58 | 59 | def map_node_to_parameter(configuration: dict, parameters_map: dict) -> dict: 60 | """Convert a configuration, where a node is associated to a value, to a configuration, where a *parameter* is associated to a value. 61 | 62 | parameters: 63 | configuration -- mapping from nodes to values 64 | parameters_map -- mapping rom nodes to parameters 65 | 66 | returns: configuration which associates a parameter to a value""" 67 | 68 | return {parameters_map[config].id : configuration[config] for config in configuration} -------------------------------------------------------------------------------- /test/converters/graphtotestsuite/testsuiteconverter/test_get_testcase_configuration.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.util.loader import load_sentence 4 | from src.util import constants 5 | 6 | from src.data.graph import Graph 7 | from src.data.test import Suite 8 | 9 | 10 | @pytest.fixture 11 | def sentence(id: str) -> dict: 12 | """Load a static sentence from the repository of sentences. 13 | 14 | parameters: 15 | id -- unique id of a sentence file 16 | 17 | returns: dictionary containing the verbatim sentence, the manually generated graph, and the manually generated input configurations""" 18 | 19 | _, _, _, graph, testsuite = load_sentence( 20 | f'{constants.SENTENCES_PATH}/sentence-{id}.json') 21 | 22 | configurations = generate_configurations(testsuite) 23 | 24 | return { 25 | 'graph': graph, 26 | 'configurations': configurations 27 | } 28 | 29 | 30 | def generate_configurations(testsuite: Suite) -> list[dict]: 31 | """Generate a list of input-configuration mappings. Each test suite contains input parameters ("conditions") and output parameters ("expected"). Each test case within a test suite maps all input parameters to a boolean value. This method generates the mappings of all input parameters to their respective values for each test case in the test suite. 32 | 33 | example: the sentence "If the button is pressed then the system shuts down." contains one input parameter (the button), one output parameter (the system), and two test cases (button is pressed and button is not pressed). This method hence generates the following configurations: [{'[the button].(is pressed)': True}, {'[the button].(is pressed)': False}] 34 | 35 | parameters: 36 | testsuite -- the test suite to generate the configurations from 37 | 38 | returns: a list of dictionaries mapping verbatim parameters to their boolean configuration value 39 | """ 40 | configurations = [] 41 | ids_of_conditions = [parameter.id for parameter in testsuite.conditions] 42 | for tc in testsuite.cases: 43 | configuration = {f'[{testsuite.get_parameter(pid).variable}].({testsuite.get_parameter(pid).condition})': tc[pid] 44 | for pid in tc if pid in ids_of_conditions} 45 | configurations.append(configuration) 46 | return configurations 47 | 48 | # exclude sentence 11 (exceptive clause not supported yet) 49 | @pytest.mark.integration 50 | @pytest.mark.parametrize('id', ['1', '1b', '1c', '2', '3', '4', '5', '6', '6b', '7', '8', '10', '12', '13', '14', '16', '17']) 51 | def test_input_config_generation(sentence): 52 | """For the manually annotated, static sentences check that the get_testcase_configuration method generates the expected set of configurations of parameters to evaluate the root cause node of a graph to both True and False. The set is supposed to be minimal (as opposed to a brute force 2^len(events) test cases).""" 53 | 54 | graph: Graph = sentence['graph'] 55 | 56 | configurations = graph.root.get_testcase_configuration(expected_outcome=True) + \ 57 | graph.root.get_testcase_configuration(expected_outcome=False) 58 | configurations_with_literal_nodes = [{str(graph.get_node(id)): config[id] 59 | for id in config} for config in configurations] 60 | 61 | assert equal_configurations( 62 | manual_configurations=sentence['configurations'], generated_configurations=configurations_with_literal_nodes) 63 | 64 | 65 | def equal_configurations(manual_configurations: list[dict], generated_configurations: list[dict]) -> bool: 66 | """Check that two lists of configurations for input parameters {parameter: True/False} are equal. 67 | 68 | parameters: 69 | manual_configurations: list of manually generated configurations 70 | generated_configurations: list of automatically generated configurations 71 | 72 | returns: True, if for every manual configuration there is exactly one equal counterpart in the list of manual configurations 73 | """ 74 | if len(manual_configurations) != len(generated_configurations): 75 | return False 76 | 77 | for mconf in manual_configurations: 78 | equivalent_configurations = [ 79 | gconf for gconf in generated_configurations if gconf == mconf] 80 | if len(equivalent_configurations) != 1: 81 | return False 82 | 83 | return True 84 | -------------------------------------------------------------------------------- /static/sentences/sentence-8.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentence": "When the red button is pushed the system shuts down and energy is saved.", 3 | "labels": [ 4 | { 5 | "id": "T2", 6 | "name": "Cause1", 7 | "begin": 5, 8 | "end": 29, 9 | "successor": { 10 | "id": "T5", 11 | "junctor": null 12 | }, 13 | "children": [ 14 | "T3", 15 | "T4" 16 | ] 17 | }, 18 | { 19 | "id": "T3", 20 | "name": "Variable", 21 | "begin": 5, 22 | "end": 19, 23 | "parent": "T2" 24 | }, 25 | { 26 | "id": "T4", 27 | "name": "Condition", 28 | "begin": 20, 29 | "end": 29, 30 | "parent": "T2" 31 | }, 32 | { 33 | "id": "T5", 34 | "name": "Effect1", 35 | "begin": 30, 36 | "end": 51, 37 | "successor": { 38 | "id": "T9", 39 | "junctor": "AND" 40 | }, 41 | "children": [ 42 | "T6", 43 | "T7" 44 | ] 45 | }, 46 | { 47 | "id": "T6", 48 | "name": "Variable", 49 | "begin": 30, 50 | "end": 40, 51 | "parent": "T5" 52 | }, 53 | { 54 | "id": "T7", 55 | "name": "Condition", 56 | "begin": 41, 57 | "end": 51, 58 | "parent": "T5" 59 | }, 60 | { 61 | "id": "T8", 62 | "name": "Conjunction", 63 | "begin": 52, 64 | "end": 55, 65 | "parent": null 66 | }, 67 | { 68 | "id": "T9", 69 | "name": "Effect2", 70 | "begin": 56, 71 | "end": 71, 72 | "successor": null, 73 | "children": [ 74 | "T10", 75 | "T11" 76 | ] 77 | }, 78 | { 79 | "id": "T10", 80 | "name": "Variable", 81 | "begin": 56, 82 | "end": 62, 83 | "parent": "T9" 84 | }, 85 | { 86 | "id": "T11", 87 | "name": "Condition", 88 | "begin": 63, 89 | "end": 71, 90 | "parent": "T9" 91 | } 92 | ], 93 | "graph": { 94 | "nodes": [ 95 | { 96 | "id": "N1", 97 | "variable": "the red button", 98 | "condition": "is pushed" 99 | }, 100 | { 101 | "id": "N2", 102 | "variable": "the system", 103 | "condition": "shuts down" 104 | }, 105 | { 106 | "id": "N3", 107 | "variable": "energy", 108 | "condition": "is saved" 109 | } 110 | ], 111 | "root": "N1", 112 | "edges": [ 113 | { 114 | "origin": "N1", 115 | "target": "N2", 116 | "negated": false 117 | }, 118 | { 119 | "origin": "N1", 120 | "target": "N3", 121 | "negated": false 122 | } 123 | ] 124 | }, 125 | "testsuite": { 126 | "conditions": [ 127 | { 128 | "id": "P1", 129 | "variable": "the red button", 130 | "condition": "is pushed" 131 | } 132 | ], 133 | "expected": [ 134 | { 135 | "id": "P2", 136 | "variable": "the system", 137 | "condition": "shuts down" 138 | }, 139 | { 140 | "id": "P3", 141 | "variable": "energy", 142 | "condition": "is saved" 143 | } 144 | ], 145 | "cases": [ 146 | { 147 | "P1": true, 148 | "P2": true, 149 | "P3": true 150 | }, 151 | { 152 | "P1": false, 153 | "P2": false, 154 | "P3": false 155 | } 156 | ] 157 | } 158 | } -------------------------------------------------------------------------------- /src/cira.py: -------------------------------------------------------------------------------- 1 | 2 | from typing import Tuple 3 | 4 | # classifiers 5 | from src.classifiers.causalitydetection.causalclassifier import CausalClassifier 6 | 7 | # converters 8 | from src.converters.sentencetolabels.labeler import Labeler 9 | from src.converters.labelstograph.graphconverter import GraphConverter 10 | from src.converters.labelstograph.eventresolver import SimpleResolver 11 | from src.converters.graphtotestsuite.testsuiteconverter import convert as convert_graph_to_testsuite 12 | 13 | from src.data.labels import Label 14 | from src.data.graph import Graph 15 | from src.data.test import Suite 16 | 17 | 18 | class CiRAConverter(): 19 | 20 | def __init__(self, classifier_causal_model_path: str, converter_s2l_model_path: str, use_GPU: bool = False): 21 | """Create a converter that exhibits the CiRA functionality (classification, labeling, CEG generation, test case generation). 22 | 23 | parameters: 24 | classifier_causal_model_path -- path to the pre-trained classification model (https://zenodo.org/record/5159501#.Ytq28ITP3-g) 25 | converter_s2l_model_path -- path to the pre-trained labeling model (https://zenodo.org/record/5550387#.Ytq3QYTP3-g) (use the model named roberta_dropout_linear_layer_multilabel.ckpt for optimal performance) 26 | use_GPU -- True if the executing system can offer CUDA to accelerate the usage of the language models 27 | """ 28 | # initialize classifiers 29 | self.classifier_causal = CausalClassifier(model_path=classifier_causal_model_path) 30 | 31 | # initialize converters 32 | self.converter_sentencetolabel = Labeler(model_path=converter_s2l_model_path, useGPU=use_GPU) 33 | self.converter_labeltograph = GraphConverter(eventresolver=SimpleResolver()) 34 | 35 | def classify(self, sentence: str) -> Tuple[bool, float]: 36 | """Classify a natural language sentence regarding whether it is causal or not. 37 | 38 | parameters: 39 | sentence -- natural language sentence in English 40 | 41 | returns: the classification whether the sentence is causal and the confidence of the classifier""" 42 | causal, confidence = self.classifier_causal.classify(sentence) 43 | return (causal, confidence) 44 | 45 | def label(self, sentence: str) -> list[Label]: 46 | """Label each token contained in a causal, natural language sentence with its respective role in the causal relationship. 47 | 48 | parameters: 49 | sentence -- natural language sentence in English 50 | 51 | returns: A list of labels 52 | """ 53 | labels: list[Label] = self.converter_sentencetolabel.label(sentence) 54 | return labels 55 | 56 | def graph(self, sentence: str, labels: list[Label]) -> Graph: 57 | """Convert a sentence and a list of labels to a cause-effect graph 58 | 59 | parameters: 60 | sentence -- natural language sentence in English 61 | labels -- list of labels representing the role of each token in the sentence in respect to the causal relationship 62 | 63 | returns: a cause-effect graph 64 | """ 65 | graph: Graph = self.converter_labeltograph.generate_graph(sentence, labels) 66 | return graph 67 | 68 | def testsuite(self, ceg: Graph) -> Suite: 69 | """Convert a cause-effect graph into a test suite containing the minimal set of test cases necessary to assert that the requirement is met. 70 | 71 | parameters: 72 | ceg -- cause-effect graph 73 | 74 | returns: a minimal test suite. 75 | """ 76 | suite: Suite = convert_graph_to_testsuite(ceg) 77 | return suite 78 | 79 | def process(self, sentence: str) -> Tuple[list[Label], Graph, Suite]: 80 | """Process a causal, natural language sentence and generate (a) a list of labels, (b) a cause-effect graph, and (c) a minimal test suite from it. 81 | 82 | parameters: 83 | sentence -- natural language sentence in English 84 | 85 | returns: Tuple containing (a) a list of labels, (b) a cause-effect graph, and (c) a minimal test suite. 86 | """ 87 | labels: list[Label] = self.converter_sentencetolabel.label(sentence) 88 | graph: Graph = self.converter_labeltograph.generate_graph(sentence, labels) 89 | suite: Suite = convert_graph_to_testsuite(graph) 90 | 91 | return (labels, graph, suite) 92 | 93 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import pkg_resources 2 | 3 | import uvicorn 4 | from fastapi import FastAPI, Request 5 | from pydantic import BaseModel 6 | from fastapi.middleware.cors import CORSMiddleware 7 | 8 | from src import model_locator 9 | from src.api.service import CiRAService, CiRAServiceImpl 10 | 11 | cira_version = pkg_resources.require("cira")[0].version 12 | 13 | description = """The CiRA API wraps the functionality of the Causality in Requirements Artifacts initiative and bundles it in one easy-to-use API. 14 | 15 | ## Functionality 16 | 17 | At the time, the following features are supported: 18 | 19 | * **classify** a single, natural language sentence as either causal or non-causal 20 | * **label** each token in a sentence regarding its role within the causal relationship 21 | * generate a cause-effect **graph** from a labeled sentence 22 | * convert a cause-effect graph into a **test suite** containing the minimal number of test cases ensuring full requirements coverage 23 | """ 24 | 25 | tags_metadata = [ 26 | { 27 | "name": "classify", 28 | "description": "Classification of a single, natural language sentence as either causal or non-causal" 29 | }, { 30 | "name": "label", 31 | "description": "Label each token in a sentence regarding its role within the causal relationship" 32 | }, { 33 | "name": "graph", 34 | "description": "Generate a cause-effect graph from a labeled sentence" 35 | }, { 36 | "name": "testsuite", 37 | "description": "Convert a cause-effect graph into a test suite" 38 | }, 39 | ] 40 | 41 | app = FastAPI( 42 | title="Causality in Requirements Artifacts - Pipeline", 43 | version=cira_version, 44 | description=description, 45 | contact={ 46 | "name": "Julian Frattini", 47 | "url": "http://www.cira.bth.se/", 48 | "email": "julian.frattini@bth.se" 49 | }, 50 | openapi_tags=tags_metadata 51 | ) 52 | PREFIX = "/api" 53 | 54 | # add CORS middleware allowing all requests from the same localhost 55 | app.add_middleware( 56 | CORSMiddleware, 57 | allow_origins=['*'], 58 | allow_credentials=True, 59 | allow_methods=["*"], 60 | allow_headers=["*"] 61 | ) 62 | 63 | cira: CiRAService = None 64 | 65 | 66 | def setup_cira(): 67 | global cira 68 | 69 | print(f'Classification model path: {model_locator.CLASSIFICATION}') 70 | print(f'Labeling model path: {model_locator.LABELING}') 71 | # generate a CiRA service implementation 72 | cira = CiRAServiceImpl(model_locator.CLASSIFICATION, model_locator.LABELING) 73 | 74 | 75 | class SentenceRequest(BaseModel): 76 | sentence: str 77 | language: str = "en" 78 | labels: list[dict] = [] 79 | graph: dict = None 80 | 81 | 82 | class ClassificationResponse(BaseModel): 83 | causal: bool 84 | confidence: float 85 | 86 | 87 | class LabelingResponse(BaseModel): 88 | labels: list[dict] 89 | 90 | 91 | class GraphResponse(BaseModel): 92 | graph: dict 93 | 94 | 95 | class TestsuiteResponse(BaseModel): 96 | suite: dict 97 | 98 | 99 | @app.get(PREFIX + "/") 100 | def root(req: Request): 101 | url_list = [ 102 | {"path": route.path, "name": route.name} for route in req.app.routes 103 | ] 104 | return url_list 105 | 106 | 107 | @app.get(PREFIX + "/health") 108 | def health(): 109 | return { 110 | "status": "up", 111 | "cira-version": cira_version 112 | } 113 | 114 | 115 | @app.put(PREFIX + '/classify', response_model=ClassificationResponse, tags=['classify']) 116 | async def create_classification(req: SentenceRequest): 117 | causal, confidence = cira.classify(req.sentence) 118 | return ClassificationResponse(causal=causal, confidence=confidence) 119 | 120 | 121 | @app.put(PREFIX + '/label', response_model=LabelingResponse, tags=['label']) 122 | async def create_labels(req: SentenceRequest): 123 | labels = cira.sentence_to_labels(sentence=req.sentence) 124 | return LabelingResponse(labels=labels) 125 | 126 | 127 | @app.put(PREFIX + '/graph', response_model=GraphResponse, tags=['graph']) 128 | async def create_graph(req: SentenceRequest): 129 | graph = cira.sentence_to_graph(sentence=req.sentence, labels=req.labels) 130 | return GraphResponse(graph=graph) 131 | 132 | 133 | @app.put(PREFIX + '/testsuite', response_model=TestsuiteResponse, tags=['testsuite']) 134 | async def create_testsuite(req: SentenceRequest): 135 | testsuite = cira.graph_to_test(graph=req.graph, sentence=req.sentence) 136 | return TestsuiteResponse(suite=testsuite) 137 | 138 | 139 | if __name__ == '__main__': 140 | setup_cira() 141 | uvicorn.run(app, host='0.0.0.0') 142 | -------------------------------------------------------------------------------- /src/converters/labelstograph/graphconverter.py: -------------------------------------------------------------------------------- 1 | import src.util.constants as consts 2 | from src.converters.labelstograph.eventconnector import connect_events 3 | from src.converters.labelstograph.eventresolver import EventResolver 4 | from src.data.graph import EventNode, Graph, Node 5 | from src.data.labels import EventLabel, Label 6 | 7 | 8 | class GraphConverter: 9 | def __init__(self, eventresolver: EventResolver): 10 | self.eventresolver: EventResolver = eventresolver 11 | 12 | def generate_graph(self, sentence: str, labels: list[Label]) -> Graph: 13 | """Convert a sentence and a list of labels into a graph 14 | 15 | parameters: 16 | sentence -- literal sentence 17 | labels -- list of interconnected labels 18 | 19 | returns: a graph representing the semantic structure of the sentence and labels""" 20 | 21 | # generate events 22 | events: list[EventNode] = generate_events(labels=labels) 23 | for event in events: 24 | self.eventresolver.resolve_event(node=event, sentence=sentence) 25 | negated_event_labels: list[EventLabel] = resolve_exceptive_negations(labels) 26 | for event_label in negated_event_labels: 27 | event = [e for e in events if (event_label in e.labels)][0] 28 | event.exceptive_negation = True 29 | 30 | # connect cause nodes with intermediate nodes representing the junctors 31 | cause_nodes = [event for event in events if event.is_cause()] 32 | causes, edgelist = connect_events(events=cause_nodes) 33 | cause_root: Node = causes[0] 34 | 35 | # connect root-cause node to effect nodes 36 | effects = [event for event in events if not event.is_cause()] 37 | for effect in effects: 38 | # check for double-negation 39 | is_double_negative = (len(causes) == 1) and (causes[0].is_negated()) 40 | is_negated=effect.is_negated() != is_double_negative 41 | edge = effect.add_incoming(child=cause_root, negated=is_negated) 42 | edgelist.append(edge) 43 | 44 | return Graph(nodes=causes+effects, root=cause_root, edges=edgelist) 45 | 46 | def generate_events(labels: list[Label]) -> list[EventNode]: 47 | """Generate an initial list of events from all event labels 48 | 49 | parameters: 50 | labels -- list of labels generated from the sentence 51 | 52 | returns list of event nodes, where each node is associated to the corresponding event label 53 | """ 54 | events: list[EventNode] = [] 55 | 56 | only_event_labels_not_unique = [label.name for label in labels if consts.is_event(label.name[:-1])] 57 | # obtain the unique event label names (e.g., Cause1, Effect2) 58 | unique_event_labels_names: list[str] = [] 59 | for label_name in only_event_labels_not_unique: 60 | if label_name not in unique_event_labels_names: 61 | unique_event_labels_names.append(label_name) 62 | 63 | for event_counter, event_label_name in enumerate(unique_event_labels_names): 64 | event_labels = [label for label in labels if label.name==event_label_name] 65 | events.append(EventNode(id=f'E{event_counter}', labels=event_labels)) 66 | return events 67 | 68 | def resolve_exceptive_negations(labels: list[Label]) -> list[EventLabel]: 69 | """Negated events are handled internally by each node resulting from an event label, but exceptive negations ("Unless A then B") have to be handled manually. This method identifies exceptive negations and identifies all events (in the form of event labels) that are affected by the additional negation. 70 | 71 | parameters: 72 | labels -- list of labels generated from the sentence 73 | 74 | returns: list of labels that are affected by an exceptive negation and hence need to be additionally negated""" 75 | exceptive_negations = [label for label in labels if label.name==consts.NEGATION and label.parent is None] 76 | 77 | all_events: list[EventLabel] = [label for label in labels if type(label)==EventLabel] 78 | all_events.sort(key=(lambda event: event.begin)) 79 | 80 | # determine all additionally negated events, i.e., the event immediately following the exceptive negation plus all additional events that are connected to that event through conjunctions 81 | negated_events: list[EventLabel] = [] 82 | for negation in exceptive_negations: 83 | affected_event = [label for label in all_events if label.begin > negation.end][0] 84 | negated_events.append(affected_event) 85 | while affected_event.successor is not None and affected_event.successor.junctor == consts.AND: 86 | affected_event = affected_event.successor.target 87 | negated_events.append(affected_event) 88 | 89 | return negated_events 90 | -------------------------------------------------------------------------------- /static/sentences/sentence-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentence": "When the red button is pushed and the power fails the system shuts down.", 3 | "labels": [ 4 | { 5 | "id": "T2", 6 | "name": "Cause1", 7 | "begin": 5, 8 | "end": 29, 9 | "successor": { 10 | "id": "T6", 11 | "junctor": "AND" 12 | }, 13 | "children": [ 14 | "T3", 15 | "T4" 16 | ] 17 | }, 18 | { 19 | "id": "T3", 20 | "name": "Variable", 21 | "begin": 5, 22 | "end": 19, 23 | "parent": "T2" 24 | }, 25 | { 26 | "id": "T4", 27 | "name": "Condition", 28 | "begin": 20, 29 | "end": 29, 30 | "parent": "T2" 31 | }, 32 | { 33 | "id": "T5", 34 | "name": "Conjunction", 35 | "begin": 30, 36 | "end": 33, 37 | "parent": null 38 | }, 39 | { 40 | "id": "T6", 41 | "name": "Cause2", 42 | "begin": 34, 43 | "end": 49, 44 | "successor": { 45 | "id": "T9", 46 | "junctor": null 47 | }, 48 | "children": [ 49 | "T7", 50 | "T8" 51 | ] 52 | }, 53 | { 54 | "id": "T7", 55 | "name": "Variable", 56 | "begin": 34, 57 | "end": 43, 58 | "parent": "T6" 59 | }, 60 | { 61 | "id": "T8", 62 | "name": "Condition", 63 | "begin": 44, 64 | "end": 49, 65 | "parent": "T6" 66 | }, 67 | { 68 | "id": "T9", 69 | "name": "Effect1", 70 | "begin": 50, 71 | "end": 71, 72 | "successor": null, 73 | "children": [ 74 | "T10", 75 | "T12" 76 | ] 77 | }, 78 | { 79 | "id": "T10", 80 | "name": "Variable", 81 | "begin": 50, 82 | "end": 60, 83 | "parent": "T9" 84 | }, 85 | { 86 | "id": "T12", 87 | "name": "Condition", 88 | "begin": 61, 89 | "end": 71, 90 | "parent": "T9" 91 | } 92 | ], 93 | "graph": { 94 | "nodes": [ 95 | { 96 | "id": "N1", 97 | "variable": "the red button", 98 | "condition": "is pushed" 99 | }, 100 | { 101 | "id": "N2", 102 | "variable": "the power", 103 | "condition": "fails" 104 | }, 105 | { 106 | "id": "N3", 107 | "conjunction": true 108 | }, 109 | { 110 | "id": "N4", 111 | "variable": "the system", 112 | "condition": "shuts down" 113 | } 114 | ], 115 | "root": "N3", 116 | "edges": [ 117 | { 118 | "origin": "N1", 119 | "target": "N3", 120 | "negated": false 121 | }, 122 | { 123 | "origin": "N2", 124 | "target": "N3", 125 | "negated": false 126 | }, 127 | { 128 | "origin": "N3", 129 | "target": "N4", 130 | "negated": false 131 | } 132 | ] 133 | }, 134 | "testsuite": { 135 | "conditions": [ 136 | { 137 | "id": "P1", 138 | "variable": "the red button", 139 | "condition": "is pushed" 140 | }, 141 | { 142 | "id": "P2", 143 | "variable": "the power", 144 | "condition": "fails" 145 | } 146 | ], 147 | "expected": [ 148 | { 149 | "id": "P3", 150 | "variable": "the system", 151 | "condition": "shuts down" 152 | } 153 | ], 154 | "cases": [ 155 | { 156 | "P1": true, 157 | "P2": true, 158 | "P3": true 159 | }, 160 | { 161 | "P1": true, 162 | "P2": false, 163 | "P3": false 164 | }, 165 | { 166 | "P1": false, 167 | "P2": true, 168 | "P3": false 169 | } 170 | ] 171 | } 172 | } -------------------------------------------------------------------------------- /static/sentences/sentence-3.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentence": "When the red button is pushed or the power fails the system shuts down.", 3 | "labels": [ 4 | { 5 | "id": "T2", 6 | "name": "Cause1", 7 | "begin": 5, 8 | "end": 29, 9 | "successor": { 10 | "id": "T6", 11 | "junctor": "OR" 12 | }, 13 | "children": [ 14 | "T3", 15 | "T4" 16 | ] 17 | }, 18 | { 19 | "id": "T3", 20 | "name": "Variable", 21 | "begin": 5, 22 | "end": 19, 23 | "parent": "T2" 24 | }, 25 | { 26 | "id": "T4", 27 | "name": "Condition", 28 | "begin": 20, 29 | "end": 29, 30 | "parent": "T2" 31 | }, 32 | { 33 | "id": "T5", 34 | "name": "Disjunction", 35 | "begin": 30, 36 | "end": 32, 37 | "parent": null 38 | }, 39 | { 40 | "id": "T6", 41 | "name": "Cause2", 42 | "begin": 33, 43 | "end": 48, 44 | "successor": { 45 | "id": "T9", 46 | "junctor": null 47 | }, 48 | "children": [ 49 | "T7", 50 | "T8" 51 | ] 52 | }, 53 | { 54 | "id": "T7", 55 | "name": "Variable", 56 | "begin": 33, 57 | "end": 42, 58 | "parent": "T6" 59 | }, 60 | { 61 | "id": "T8", 62 | "name": "Condition", 63 | "begin": 43, 64 | "end": 48, 65 | "parent": "T6" 66 | }, 67 | { 68 | "id": "T9", 69 | "name": "Effect1", 70 | "begin": 49, 71 | "end": 70, 72 | "successor": null, 73 | "children": [ 74 | "T10", 75 | "T12" 76 | ] 77 | }, 78 | { 79 | "id": "T10", 80 | "name": "Variable", 81 | "begin": 49, 82 | "end": 59, 83 | "parent": "T9" 84 | }, 85 | { 86 | "id": "T12", 87 | "name": "Condition", 88 | "begin": 60, 89 | "end": 70, 90 | "parent": "T9" 91 | } 92 | ], 93 | "graph": { 94 | "nodes": [ 95 | { 96 | "id": "N1", 97 | "variable": "the red button", 98 | "condition": "is pushed" 99 | }, 100 | { 101 | "id": "N2", 102 | "variable": "the power", 103 | "condition": "fails" 104 | }, 105 | { 106 | "id": "N3", 107 | "conjunction": false 108 | }, 109 | { 110 | "id": "N4", 111 | "variable": "the system", 112 | "condition": "shuts down" 113 | } 114 | ], 115 | "root": "N3", 116 | "edges": [ 117 | { 118 | "origin": "N1", 119 | "target": "N3", 120 | "negated": false 121 | }, 122 | { 123 | "origin": "N2", 124 | "target": "N3", 125 | "negated": false 126 | }, 127 | { 128 | "origin": "N3", 129 | "target": "N4", 130 | "negated": false 131 | } 132 | ] 133 | }, 134 | "testsuite": { 135 | "conditions": [ 136 | { 137 | "id": "P1", 138 | "variable": "the red button", 139 | "condition": "is pushed" 140 | }, 141 | { 142 | "id": "P2", 143 | "variable": "the power", 144 | "condition": "fails" 145 | } 146 | ], 147 | "expected": [ 148 | { 149 | "id": "P3", 150 | "variable": "the system", 151 | "condition": "shuts down" 152 | } 153 | ], 154 | "cases": [ 155 | { 156 | "P1": false, 157 | "P2": false, 158 | "P3": false 159 | }, 160 | { 161 | "P1": true, 162 | "P2": false, 163 | "P3": true 164 | }, 165 | { 166 | "P1": false, 167 | "P2": true, 168 | "P3": true 169 | } 170 | ] 171 | } 172 | } -------------------------------------------------------------------------------- /test/converters/labelstograph/eventresolver/test_eventresolver.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.converters.labelstograph.eventresolver import SimpleResolver 4 | 5 | from src.data.graph import EventNode 6 | from src.data.labels import EventLabel, SubLabel 7 | 8 | @pytest.mark.integration 9 | def test_simple(): 10 | resolver = SimpleResolver() 11 | sentence = "When the red button is pushed the system shuts down." 12 | 13 | c1 = EventLabel(id='L1', name='Cause1', begin=5, end=29) 14 | c1_v = SubLabel(id='L2', name='Variable', begin=5, end=19) 15 | c1_c = SubLabel(id='L3', name='Condition', begin=20, end=29) 16 | c1.add_child(c1_c) 17 | c1.add_child(c1_v) 18 | 19 | event1 = EventNode(id='N1', labels=[c1]) 20 | resolver.resolve_event(node=event1, sentence=sentence) 21 | 22 | assert event1.variable == "the red button" 23 | assert event1.condition == "is pushed" 24 | 25 | @pytest.mark.integration 26 | def test_split(): 27 | resolver = SimpleResolver() 28 | sentence = "When the red is button pushed the system shuts down." 29 | 30 | c1 = EventLabel(id='L1', name='Cause1', begin=5, end=29) 31 | c1_v1 = SubLabel(id='L2', name='Variable', begin=5, end=12) 32 | c1_c1 = SubLabel(id='L3', name='Condition', begin=13, end=15) 33 | c1_v2 = SubLabel(id='L4', name='Variable', begin=16, end=22) 34 | c1_c2 = SubLabel(id='L5', name='Condition', begin=23, end=29) 35 | c1.add_child(c1_c1) 36 | c1.add_child(c1_v1) 37 | c1.add_child(c1_c2) 38 | c1.add_child(c1_v2) 39 | 40 | event1 = EventNode(id='N1', labels=[c1]) 41 | resolver.resolve_event(node=event1, sentence=sentence) 42 | 43 | assert event1.variable == "the red button" 44 | assert event1.condition == "is pushed" 45 | 46 | @pytest.mark.integration 47 | def test_move1_variable(): 48 | resolver = SimpleResolver() 49 | sentence = "If the button is pressed or released" 50 | 51 | c1 = EventLabel(id='L1', name='Cause1', begin=3, end=24) 52 | c1.add_child(SubLabel(id='L2', name='Variable', begin=3, end=13)) 53 | c1.add_child(SubLabel(id='L3', name='Condition', begin=14, end=24)) 54 | c2 = EventLabel(id='L4', name='Cause1', begin=28, end=36) 55 | c2.add_child(SubLabel(id='L5', name='Condition', begin=28, end=36)) 56 | 57 | c1.set_successor(c2, 'OR') 58 | 59 | event2 = EventNode(id='N1', labels=[c2]) 60 | resolver.resolve_event(node=event2, sentence=sentence) 61 | 62 | assert event2.variable == "the button" 63 | 64 | @pytest.mark.integration 65 | def test_move1_condition(): 66 | resolver = SimpleResolver() 67 | sentence = "If the button or the link is pressed" 68 | 69 | c1 = EventLabel(id='L1', name='Cause1', begin=3, end=13) 70 | c1.add_child(SubLabel(id='L2', name='Variable', begin=3, end=13)) 71 | c2 = EventLabel(id='L4', name='Cause1', begin=17, end=36) 72 | c1.add_child(SubLabel(id='L2', name='Variable', begin=17, end=25)) 73 | c2.add_child(SubLabel(id='L5', name='Condition', begin=26, end=36)) 74 | 75 | c1.set_successor(c2, 'OR') 76 | 77 | event1 = EventNode(id='N1', labels=[c1]) 78 | resolver.resolve_event(node=event1, sentence=sentence) 79 | 80 | assert event1.condition == "is pressed" 81 | 82 | @pytest.mark.integration 83 | def test_join_two_labels(): 84 | # this test asserts that an event node with two distinct event labels still resolves the event correctly 85 | resolver = SimpleResolver() 86 | sentence = "The button that is pressed" 87 | 88 | c1_1 = EventLabel(id='L1', name='Cause1', begin=0, end=10) 89 | c1_1.add_child(SubLabel(id='L2', name='Variable', begin=0, end=10)) 90 | c1_2 = EventLabel(id='L3', name='Cause1', begin=16, end=26) 91 | c1_2.add_child(SubLabel(id='L4', name='Condition', begin=16, end=26)) 92 | 93 | c1_1.set_successor(c1_2, junctor='MERGE') 94 | 95 | event = EventNode(id='N1', labels=[c1_1, c1_2]) 96 | resolver.resolve_event(node=event, sentence=sentence) 97 | 98 | assert event.variable == 'The button' 99 | assert event.condition == 'is pressed' 100 | 101 | @pytest.mark.integration 102 | def test_join_two_sublabels(): 103 | resolver = SimpleResolver() 104 | sentence = "Data transmission is only possible" 105 | 106 | e1_1 = EventLabel(id='L1', name='Effect1', begin=0, end=20) 107 | e1_1.add_child(SubLabel(id='L2', name='Variable', begin=0, end=17)) 108 | e1_1.add_child(SubLabel(id='L3', name='Condition', begin=18, end=20)) 109 | e1_2 = EventLabel(id='L4', name='Effect1', begin=26, end=34) 110 | e1_2.add_child(SubLabel(id='L5', name='Condition', begin=26, end=34)) 111 | 112 | e1_1.set_successor(e1_2, junctor='MERGE') 113 | 114 | event = EventNode(id='N1', labels=[e1_1, e1_2]) 115 | resolver.resolve_event(node=event, sentence=sentence) 116 | 117 | assert event.variable == 'Data transmission' 118 | assert event.condition == 'is possible' -------------------------------------------------------------------------------- /test/app/test_app_integrated.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fastapi.testclient import TestClient 4 | from fastapi import status 5 | 6 | from src import model_locator 7 | import app 8 | from src.api.service import CiRAServiceImpl 9 | 10 | sentence = "If the button is pressed then the system shuts down." 11 | API_URL = 'http://localhost:8000/api/' 12 | 13 | labels: list[dict] = [ 14 | {'id': 'L0', 'name': 'Cause1', 'begin': 3, 'end': 24, 'successor': { 15 | 'id': 'L1', 'junctor': None}, 'children': ['L2', 'L4']}, 16 | {'id': 'L1', 'name': 'Effect1', 'begin': 30, 'end': 51, 17 | 'successor': None, 'children': ['L3', 'L5']}, 18 | {'id': 'L2', 'name': 'Variable', 'begin': 3, 'end': 13, 'parent': 'L0'}, 19 | {'id': 'L3', 'name': 'Variable', 'begin': 30, 'end': 40, 'parent': 'L1'}, 20 | {'id': 'L4', 'name': 'Condition', 'begin': 14, 'end': 24, 'parent': 'L0'}, 21 | {'id': 'L5', 'name': 'Condition', 'begin': 41, 'end': 51, 'parent': 'L1'}, 22 | ] 23 | 24 | graph = { 25 | 'nodes': [{'id': 'E0', 'variable': 'the button', 'condition': 'is pressed'}, {'id': 'E1', 'variable': 'the system', 'condition': 'shuts down'}], 26 | 'root': 'E0', 27 | 'edges': [{'origin': 'E0', 'target': 'E1', 'negated': False}] 28 | } 29 | 30 | suite = { 31 | 'conditions': [{'id': 'P0', 'variable': 'the button', 32 | 'condition': 'is pressed'}], 33 | 'expected': [{'id': 'P1', 'variable': 'the system', 34 | 'condition': 'shuts down'}], 35 | 'cases': [{'P0': True, 'P1': True}, {'P0': False, 'P1': False}] 36 | } 37 | 38 | 39 | @pytest.fixture(scope="module") 40 | def client() -> TestClient: 41 | # create the system under test 42 | app.cira = CiRAServiceImpl( 43 | model_classification=model_locator.CLASSIFICATION, model_labeling=model_locator.LABELING) 44 | 45 | client = TestClient(app.app) 46 | return client 47 | 48 | 49 | @pytest.mark.system 50 | def test_classification(client): 51 | response = client.put( 52 | f'{API_URL}classify', json={"sentence": sentence}) 53 | 54 | assert response.status_code == status.HTTP_200_OK 55 | 56 | body = response.json() 57 | assert body['causal'] == True 58 | assert body['confidence'] > 0.9 59 | 60 | 61 | @pytest.mark.system 62 | def test_classification_empty_sentence(client): 63 | response = client.put( 64 | f'{API_URL}classify', json={"sentence": ""}) 65 | 66 | assert response.status_code == status.HTTP_200_OK 67 | 68 | body = response.json() 69 | assert body['causal'] == False 70 | assert body['confidence'] > 0.9 71 | 72 | 73 | @pytest.mark.system 74 | def test_classification_missing_sentence(client): 75 | response = client.put( 76 | f'{API_URL}classify', json={}) 77 | 78 | assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY 79 | 80 | 81 | @pytest.mark.system 82 | def test_classification_missing_sentence_recovery(client): 83 | response = client.put( 84 | f'{API_URL}classify', json={}) 85 | assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY 86 | 87 | # assert that after a failed request a valid request will succeed 88 | response = client.put( 89 | f'{API_URL}classify', json={"sentence": sentence}) 90 | assert response.status_code == status.HTTP_200_OK 91 | 92 | 93 | @pytest.mark.system 94 | def test_labeling(client): 95 | response = client.put( 96 | f'{API_URL}label', json={"sentence": sentence}) 97 | 98 | assert response.status_code == status.HTTP_200_OK 99 | 100 | body = response.json() 101 | assert body['labels'] == labels 102 | 103 | 104 | @pytest.mark.system 105 | def test_labeling_emtpy_sentence(client): 106 | response = client.put( 107 | f'{API_URL}label', json={"sentence": ""}) 108 | 109 | assert response.status_code == status.HTTP_200_OK 110 | 111 | body = response.json() 112 | assert body['labels'] == [] 113 | 114 | 115 | @pytest.mark.system 116 | def test_graph(client): 117 | response = client.put( 118 | f'{API_URL}graph', json={"sentence": sentence}) 119 | 120 | assert response.status_code == status.HTTP_200_OK 121 | 122 | body = response.json() 123 | assert body['graph'] == graph 124 | 125 | 126 | @pytest.mark.system 127 | def test_suite(client): 128 | response = client.put( 129 | f'{API_URL}testsuite', json={"sentence": sentence, "graph": graph}) 130 | 131 | assert response.status_code == status.HTTP_200_OK 132 | 133 | body = response.json() 134 | assert body['suite'] == suite 135 | 136 | 137 | @pytest.mark.system 138 | def test_suite_missing_graph(client): 139 | response = client.put( 140 | f'{API_URL}testsuite', json={"sentence": sentence}) 141 | 142 | assert response.status_code == status.HTTP_200_OK 143 | 144 | body = response.json() 145 | assert body['suite'] == suite 146 | -------------------------------------------------------------------------------- /src/converters/labelstograph/eventconnector.py: -------------------------------------------------------------------------------- 1 | from typing import Tuple 2 | from src.data.graph import Node, EventNode, IntermediateNode, Edge 3 | 4 | import src.util.constants as consts 5 | 6 | def connect_events(events: list[EventNode]) -> Tuple[list[Node], list[Edge]]: 7 | """Connect a list of events based on the relationships between them to a tree, where the leaf nodes represent events and all intermediate nodes represent junctors 8 | 9 | parameters: 10 | events -- list of events to connect (applicable for neighboring events connected with junctors) 11 | 12 | returns: minimal list of nodes representing a tree (with the root node at position 0) 13 | """ 14 | if len(events) == 1: 15 | # in case there is only one event node, there are no junctors to resolve and events to connect 16 | return (events, []) 17 | 18 | # in case there are at least two event nodes, resolve the junctors by introducing intermediate nodes 19 | junctor_map: dict = get_junctors(events=events) 20 | _, edges = generate_initial_nodenet(events=events, junctor_map=junctor_map) 21 | edgelist: list[Edge] = edges 22 | 23 | # ensure that every leaf node has exactly one parent 24 | all_removable_edges: list[Edge] = [] 25 | all_new_edges: list[Edge] = [] 26 | for event in events: 27 | removable_edges, new_edges = event.condense() 28 | all_removable_edges = all_removable_edges + removable_edges 29 | all_new_edges = all_new_edges + new_edges 30 | # remove the deleted edges from the edgelist 31 | for edge in all_removable_edges: 32 | edgelist.remove(edge) 33 | # add the new edges to the edgelist 34 | edgelist = edgelist + all_new_edges 35 | 36 | # obtain the root node: 37 | root = events[0].get_root() 38 | return (root.flatten(), edgelist) 39 | 40 | 41 | def get_junctors(events: list[EventNode]) -> dict: 42 | """Obtain a dictionary that maps two adjacent event nodes to their respective junctor (conjunction or disjunction). If no explicit junctor is given, (recursively) take the junctor between the next pair of event nodes 43 | 44 | parameters: 45 | events -- list of event nodes 46 | 47 | returns: map from pairs of event node ids to a junctor (AND/OR)""" 48 | 49 | junctor_map = {} 50 | 51 | # get the starting node (the cause event node associated to the event label with the lowest begin index) 52 | current_node: EventNode = sorted(events, key=lambda event: event.labels[0].begin, reverse=False)[0] 53 | while current_node.labels[-1].successor is not None: 54 | label1 = current_node.labels[-1] 55 | label2 = label1.successor.target 56 | # only consider junctors between labels of the same type (Cause or effect) 57 | if label1.is_cause() != label2.is_cause(): 58 | break 59 | 60 | # obtain the node with which the current node is joined and denote the junctor 61 | next_node = [event for event in events if label2 in event.labels][0] 62 | junctor_map[(current_node.id, next_node.id)] = label1.successor.junctor 63 | # continue with that node 64 | current_node = next_node 65 | 66 | # fill all implicit junctors by traversing the junctor map in reverse order (as explicit junctors tend to appear towards the and, like in "If a [?], b [?], c [?], [AND] d, then e.") 67 | previous = consts.AND 68 | junctor_map_reverse = list(junctor_map.keys())[::-1] 69 | for nodepair in junctor_map_reverse: 70 | if junctor_map[nodepair] is None: 71 | junctor_map[nodepair] = previous 72 | 73 | previous = junctor_map[nodepair] 74 | return junctor_map 75 | 76 | 77 | def generate_initial_nodenet(events: list[Node], junctor_map: dict) -> Tuple[list[IntermediateNode], list[Edge]]: 78 | """Generates the intermediate nodes that initially connect the list of events. The type of intermediate node (conjunction/disjunction) is derived from the junctor map. 79 | 80 | parameters: 81 | events -- list of events to connect 82 | junctor_map -- dict which connects every two adjacent events with a junctor ('AND'/'OR') 83 | 84 | returns: a list of intermediate nodes connecting the event nodes""" 85 | junctors: list[IntermediateNode] = [] 86 | edgelist: list[Edge] = [] 87 | 88 | for index, junctor in enumerate(junctor_map): 89 | intermediate = IntermediateNode( 90 | id=f'I{index}', 91 | conjunction=(junctor_map[junctor]==consts.AND), 92 | precedence=(junctor_map[junctor].startswith('P'))) 93 | joined_nodes: list[EventNode] = [event for event in events if (event.id in junctor)] 94 | for node in joined_nodes: 95 | is_negated: bool = (bool) (node.is_negated()) 96 | edge = intermediate.add_incoming(child=node, negated=is_negated) 97 | edgelist.append(edge) 98 | junctors.append(intermediate) 99 | 100 | return (junctors,edgelist) 101 | -------------------------------------------------------------------------------- /static/sentences/sentence-10.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentence": "Unless the red button is pushed and the power fails the system continues to operate.", 3 | "labels": [ 4 | { 5 | "id": "T2", 6 | "name": "Negation", 7 | "begin": 0, 8 | "end": 6, 9 | "parent": null 10 | }, 11 | { 12 | "id": "T3", 13 | "name": "Cause1", 14 | "begin": 7, 15 | "end": 31, 16 | "successor": { 17 | "id": "T7", 18 | "junctor": "AND" 19 | }, 20 | "children": [ 21 | "T4", 22 | "T5" 23 | ] 24 | }, 25 | { 26 | "id": "T4", 27 | "name": "Variable", 28 | "begin": 7, 29 | "end": 21, 30 | "parent": "T3" 31 | }, 32 | { 33 | "id": "T5", 34 | "name": "Condition", 35 | "begin": 22, 36 | "end": 31, 37 | "parent": "T3" 38 | }, 39 | { 40 | "id": "T6", 41 | "name": "Conjunction", 42 | "begin": 32, 43 | "end": 35, 44 | "parent": null 45 | }, 46 | { 47 | "id": "T7", 48 | "name": "Cause2", 49 | "begin": 36, 50 | "end": 51, 51 | "successor": { 52 | "id": "T10", 53 | "junctor": null 54 | }, 55 | "children": [ 56 | "T8", 57 | "T9" 58 | ] 59 | }, 60 | { 61 | "id": "T8", 62 | "name": "Variable", 63 | "begin": 36, 64 | "end": 45, 65 | "parent": "T7" 66 | }, 67 | { 68 | "id": "T9", 69 | "name": "Condition", 70 | "begin": 46, 71 | "end": 51, 72 | "parent": "T7" 73 | }, 74 | { 75 | "id": "T10", 76 | "name": "Effect1", 77 | "begin": 52, 78 | "end": 83, 79 | "successor": null, 80 | "children": [ 81 | "T11", 82 | "T12" 83 | ] 84 | }, 85 | { 86 | "id": "T11", 87 | "name": "Variable", 88 | "begin": 52, 89 | "end": 62, 90 | "parent": "T10" 91 | }, 92 | { 93 | "id": "T12", 94 | "name": "Condition", 95 | "begin": 63, 96 | "end": 83, 97 | "parent": "T10" 98 | } 99 | ], 100 | "graph": { 101 | "nodes": [ 102 | { 103 | "id": "N1", 104 | "variable": "the red button", 105 | "condition": "is pushed" 106 | }, 107 | { 108 | "id": "N2", 109 | "variable": "the power", 110 | "condition": "fails" 111 | }, 112 | { 113 | "id": "N3", 114 | "conjunction": true 115 | }, 116 | { 117 | "id": "N4", 118 | "variable": "the system", 119 | "condition": "continues to operate" 120 | } 121 | ], 122 | "root": "N3", 123 | "edges": [ 124 | { 125 | "origin": "N1", 126 | "target": "N3", 127 | "negated": true 128 | }, 129 | { 130 | "origin": "N2", 131 | "target": "N3", 132 | "negated": true 133 | }, 134 | { 135 | "origin": "N3", 136 | "target": "N4", 137 | "negated": false 138 | } 139 | ] 140 | }, 141 | "testsuite": { 142 | "conditions": [ 143 | { 144 | "id": "P1", 145 | "variable": "the red button", 146 | "condition": "is pushed" 147 | }, 148 | { 149 | "id": "P2", 150 | "variable": "the power", 151 | "condition": "fails" 152 | } 153 | ], 154 | "expected": [ 155 | { 156 | "id": "P3", 157 | "variable": "the system", 158 | "condition": "continues to operate" 159 | } 160 | ], 161 | "cases": [ 162 | { 163 | "P1": false, 164 | "P2": false, 165 | "P3": true 166 | }, 167 | { 168 | "P1": true, 169 | "P2": false, 170 | "P3": false 171 | }, 172 | { 173 | "P1": false, 174 | "P2": true, 175 | "P3": false 176 | } 177 | ] 178 | } 179 | } -------------------------------------------------------------------------------- /test/data/labels/test_labels_serialization.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.labels import EventLabel, SubLabel 4 | 5 | @pytest.mark.unit 6 | def test_sublabel_serialization_parentless(): 7 | label = SubLabel(id='L1', name='Variable', begin=10, end=15) 8 | serialized = label.to_dict() 9 | expected = { 10 | 'id': 'L1', 11 | 'name': 'Variable', 12 | 'begin': 10, 13 | 'end': 15, 14 | 'parent': None 15 | } 16 | assert expected == serialized 17 | 18 | @pytest.mark.unit 19 | def test_sublabel_serialization_parent(): 20 | label = SubLabel(id='L1', name='Variable', begin=10, end=15) 21 | parent = EventLabel(id='L2', name='Cause1', begin=10, end=30) 22 | parent.add_child(child=label) 23 | serialized = label.to_dict() 24 | 25 | expected = { 26 | 'id': 'L1', 27 | 'name': 'Variable', 28 | 'begin': 10, 29 | 'end': 15, 30 | 'parent': 'L2' 31 | } 32 | assert expected == serialized 33 | 34 | @pytest.mark.unit 35 | def test_evetlabel_serialization_child(): 36 | cause1 = EventLabel(id='L1', name='Cause1', begin=0, end=30) 37 | variable = SubLabel(id='L2', name='Variable', begin=0, end=10) 38 | cause1.add_child(variable) 39 | serialized = cause1.to_dict() 40 | 41 | expected = { 42 | 'id': 'L1', 43 | 'name': 'Cause1', 44 | 'begin': 0, 45 | 'end': 30, 46 | 'successor': None, 47 | 'children': ['L2'] 48 | } 49 | assert expected == serialized 50 | 51 | @pytest.mark.unit 52 | def test_evetlabel_serialization_children(): 53 | cause1 = EventLabel(id='L1', name='Cause1', begin=0, end=30) 54 | variable = SubLabel(id='L2', name='Variable', begin=0, end=10) 55 | condition = SubLabel(id='L3', name='Condition', begin=11, end=30) 56 | cause1.add_child(variable) 57 | cause1.add_child(condition) 58 | serialized = cause1.to_dict() 59 | 60 | expected = { 61 | 'id': 'L1', 62 | 'name': 'Cause1', 63 | 'begin': 0, 64 | 'end': 30, 65 | 'successor': None, 66 | 'children': ['L2', 'L3'] 67 | } 68 | assert expected == serialized 69 | 70 | @pytest.mark.unit 71 | def test_evetlabel_serialization_successor(): 72 | cause1 = EventLabel(id='L1', name='Cause1', begin=0, end=30) 73 | cause2 = EventLabel(id='L2', name='Cause2', begin=31, end=60) 74 | cause1.set_successor(cause2, junctor='AND') 75 | serialized = cause1.to_dict() 76 | 77 | expected = { 78 | 'id': 'L1', 79 | 'name': 'Cause1', 80 | 'begin': 0, 81 | 'end': 30, 82 | 'successor': { 83 | 'id': 'L2', 84 | 'junctor': 'AND' 85 | }, 86 | 'children': [] 87 | } 88 | assert expected == serialized 89 | 90 | @pytest.mark.unit 91 | def test_evetlabel_serialization_predecessor(): 92 | cause1 = EventLabel(id='L1', name='Cause1', begin=0, end=30) 93 | cause2 = EventLabel(id='L2', name='Cause2', begin=31, end=60) 94 | cause1.set_successor(cause2, junctor='OR') 95 | serialized = cause2.to_dict() 96 | 97 | expected = { 98 | 'id': 'L2', 99 | 'name': 'Cause2', 100 | 'begin': 31, 101 | 'end': 60, 102 | 'successor': None, 103 | 'children': [] 104 | } 105 | assert expected == serialized 106 | 107 | @pytest.mark.unit 108 | def test_evetlabel_serialization_successor_predecessor(): 109 | cause1 = EventLabel(id='L1', name='Cause1', begin=0, end=30) 110 | cause2 = EventLabel(id='L2', name='Cause2', begin=31, end=60) 111 | cause3 = EventLabel(id='L3', name='Cause3', begin=61, end=90) 112 | cause1.set_successor(cause2, junctor='AND') 113 | cause2.set_successor(cause3, junctor='OR') 114 | serialized = cause2.to_dict() 115 | 116 | expected = { 117 | 'id': 'L2', 118 | 'name': 'Cause2', 119 | 'begin': 31, 120 | 'end': 60, 121 | 'successor': { 122 | 'id': 'L3', 123 | 'junctor': 'OR' 124 | }, 125 | 'children': [] 126 | } 127 | assert expected == serialized 128 | 129 | @pytest.mark.unit 130 | def test_evetlabel_serialization_successor_predecessor_children(): 131 | cause2 = EventLabel(id='L2', name='Cause2', begin=31, end=60) 132 | 133 | variable = SubLabel(id='L4', name='Variable', begin=0, end=10) 134 | condition = SubLabel(id='L5', name='Condition', begin=11, end=30) 135 | cause2.add_child(variable) 136 | cause2.add_child(condition) 137 | 138 | cause1 = EventLabel(id='L1', name='Cause1', begin=0, end=30) 139 | cause3 = EventLabel(id='L3', name='Cause3', begin=61, end=90) 140 | cause1.set_successor(cause2, junctor='AND') 141 | cause2.set_successor(cause3, junctor='OR') 142 | serialized = cause2.to_dict() 143 | 144 | expected = { 145 | 'id': 'L2', 146 | 'name': 'Cause2', 147 | 'begin': 31, 148 | 'end': 60, 149 | 'successor': { 150 | 'id': 'L3', 151 | 'junctor': 'OR' 152 | }, 153 | 'children': ['L4', 'L5'] 154 | } 155 | assert expected == serialized -------------------------------------------------------------------------------- /test/converters/sentencetolabels/labelingconverter/test_connect_labels.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from typing import Tuple 3 | 4 | from src.data.labels import Label, EventLabel, SubLabel 5 | from src.converters.sentencetolabels.labelingconverter import connect_labels 6 | 7 | @pytest.fixture 8 | def two_labels(event: Tuple[int, int], variable: Tuple[int, int]) -> list[Label]: 9 | """Create a list of two labels (one event, one variable) with a given begin and end point 10 | 11 | parameters: 12 | event -- begin and end point of the event label 13 | variable -- begin and end point of the variable label 14 | 15 | returns: list of two labels with the given begin and end point 16 | """ 17 | return [ 18 | EventLabel(id='T1', name='Cause1', begin=event[0], end=event[1]), 19 | SubLabel(id='T2', name='Variable', begin=variable[0], end=variable[1]) 20 | ] 21 | 22 | @pytest.mark.integration 23 | @pytest.mark.parametrize('event, variable, expected', [ 24 | ((0, 5), (0, 3), 1), 25 | ((0, 5), (3, 5), 1), 26 | ((0, 5), (0, 5), 1), 27 | ((1, 5), (0, 3), 0), 28 | ((0, 4), (3, 5), 0), 29 | ]) 30 | def test_connect_childcount(two_labels, expected): 31 | """Test that connecting two labels with a given configuration of begin and end points results in the expected number of children 32 | 33 | parameters: 34 | two_labels -- fixture creating a list of two labels with the parametrized configuration 35 | expected -- number of children that the first label is supposed to have after the connection 36 | """ 37 | connect_labels(two_labels) 38 | 39 | assert len(two_labels[0].children) == expected 40 | 41 | if expected == 1: 42 | assert two_labels[0].children[0] == two_labels[1] 43 | assert two_labels[1].parent == two_labels[0] 44 | 45 | @pytest.mark.integration 46 | def test_connect_neighbors(): 47 | c1 = EventLabel(id='T1', name='Cause1', begin=0, end=5) 48 | c2 = EventLabel(id='T2', name='Cause2', begin=6, end=10) 49 | e1 = EventLabel(id='T4', name='Effect1', begin=11, end=20) 50 | labels = [c2, e1, c1] 51 | 52 | connect_labels(labels) 53 | 54 | assert c1.predecessor == None 55 | assert c1.successor.target == c2 56 | assert c2.predecessor.origin == c1 57 | assert c2.successor.target == e1 58 | assert e1.predecessor.origin == c2 59 | assert e1.successor == None 60 | 61 | @pytest.mark.integration 62 | def test_junctors(): 63 | c1 = EventLabel(id='T1', name='Cause1', begin=0, end=5) 64 | conj = SubLabel(id='T2', name='Conjunction', begin=6, end=9) 65 | c2 = EventLabel(id='T3', name='Cause2', begin=10, end=15) 66 | disj = SubLabel(id='T4', name='Disjunction', begin=16, end=18) 67 | c3 = EventLabel(id='T5', name='Cause2', begin=19, end=24) 68 | c4 = EventLabel(id='T5', name='Cause2', begin=25, end=30) 69 | labels = [c1, conj, c2, disj, c3, c4] 70 | 71 | connect_labels(labels) 72 | 73 | assert c1.successor.junctor == 'AND' 74 | assert c2.successor.junctor == 'OR' 75 | assert c3.successor.junctor == None 76 | 77 | @pytest.mark.integration 78 | def test_junctors_overruled_precedence1(): 79 | """Usually, we interpret junctors like logical operators and hence assume that AND has a higher precedence than OR. For example, "A or B and C" is interpreted as "A or (B and C)". However, the precedence can be overruled in a case like "A and either B or C", which is interpreted as "A and (B or C)". The indicator for the overruled precedence is the existence of both a conjunction and disjunction between two events 80 | """ 81 | c1 = EventLabel(id='T1', name='Cause1', begin=0, end=5) 82 | # indicator for the overruled precedence: conjunction and disjunction between cause 1 and 2 83 | conj = SubLabel(id='T2-1', name='Conjunction', begin=6, end=7) 84 | disjp = SubLabel(id='T2-2', name='Disjunction', begin=8, end=9) 85 | c2 = EventLabel(id='T3', name='Cause2', begin=10, end=15) 86 | disj = SubLabel(id='T4', name='Disjunction', begin=16, end=18) 87 | c3 = EventLabel(id='T5', name='Cause2', begin=19, end=24) 88 | labels = [c1, conj, disjp, c2, disj, c3] 89 | 90 | connect_labels(labels) 91 | 92 | assert c1.successor.junctor == 'AND' 93 | assert c2.successor.junctor == 'POR' 94 | 95 | #either A or B and C 96 | @pytest.mark.integration 97 | @pytest.mark.skip(reason="UNCLEAR_REQUIREMENT: how this case is to be resolved linguistically is not yet clear.") 98 | def test_junctors_overruled_precedence2(): 99 | """Another version of the overruled precedence is if the indicator for the overruling precedence (either) being in the beginning, e.g., "When either A or B and C." It is yet unclear how this case should be resolved.""" 100 | 101 | disjp = SubLabel(id='T0', name='Disjunction', begin=8, end=9) 102 | c1 = EventLabel(id='T1', name='Cause1', begin=0, end=5) 103 | disj = SubLabel(id='T2', name='Disjunction', begin=6, end=7) 104 | c2 = EventLabel(id='T3', name='Cause2', begin=10, end=15) 105 | conj = SubLabel(id='T4', name='Conjunction', begin=16, end=18) 106 | c3 = EventLabel(id='T5', name='Cause2', begin=19, end=24) 107 | labels = [disjp, c1, disj, c2, conj, c3] 108 | 109 | connect_labels(labels) 110 | 111 | assert False -------------------------------------------------------------------------------- /test/data/labels/test_labels_deserialization.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.labels import EventLabel, SubLabel, from_dict 4 | 5 | 6 | @pytest.mark.unit 7 | def test_sublabel_deserialization(): 8 | serialized = [{ 9 | 'id': 'L1', 10 | 'name': 'Variable', 11 | 'begin': 10, 12 | 'end': 15, 13 | 'parent': None 14 | }] 15 | deserialized = from_dict(serialized) 16 | 17 | expected = [SubLabel(id='L1', name='Variable', begin=10, end=15)] 18 | assert deserialized == expected 19 | 20 | 21 | @pytest.mark.unit 22 | def test_eventlabel_deserialization_child(): 23 | serialized = [ 24 | { 25 | 'id': 'L1', 26 | 'name': 'Variable', 27 | 'begin': 10, 28 | 'end': 15, 29 | 'parent': 'L2' 30 | }, { 31 | 'id': 'L2', 32 | 'name': 'Cause1', 33 | 'begin': 10, 34 | 'end': 30, 35 | 'children': ['L1'], 36 | 'predecessor': None, 37 | 'successor': None 38 | } 39 | ] 40 | deserialized = from_dict(serialized) 41 | 42 | l1 = SubLabel(id='L1', name='Variable', begin=10, end=15) 43 | l2 = EventLabel(id='L2', name='Cause1', begin=10, end=30) 44 | l2.add_child(l1) 45 | expected = [l1, l2] 46 | assert deserialized == expected 47 | 48 | @pytest.mark.unit 49 | def test_eventlabel_deserialization_children(): 50 | serialized = [ 51 | { 52 | 'id': 'L1', 53 | 'name': 'Variable', 54 | 'begin': 10, 55 | 'end': 15, 56 | 'parent': 'L2' 57 | }, { 58 | 'id': 'L2', 59 | 'name': 'Cause1', 60 | 'begin': 10, 61 | 'end': 30, 62 | 'children': ['L1', 'L3'], 63 | 'predecessor': None, 64 | 'successor': None 65 | }, { 66 | 'id': 'L3', 67 | 'name': 'Condition', 68 | 'begin': 16, 69 | 'end': 30, 70 | 'parent': 'L3' 71 | } 72 | ] 73 | deserialized = from_dict(serialized) 74 | 75 | l1 = SubLabel(id='L1', name='Variable', begin=10, end=15) 76 | l3 = SubLabel(id='L3', name='Condition', begin=16, end=30) 77 | l2 = EventLabel(id='L2', name='Cause1', begin=10, end=30) 78 | l2.add_child(l1) 79 | l2.add_child(l3) 80 | expected = [l1, l2, l3] 81 | assert deserialized == expected 82 | 83 | @pytest.mark.unit 84 | def test_eventlabel_deserialization_successor(): 85 | serialized = [ 86 | { 87 | 'id': 'L1', 88 | 'name': 'Cause1', 89 | 'begin': 0, 90 | 'end': 30, 91 | 'children': [], 92 | 'successor': { 93 | 'id': 'L2', 94 | 'junctor': 'AND' 95 | } 96 | }, { 97 | 'id': 'L2', 98 | 'name': 'Cause2', 99 | 'begin': 31, 100 | 'end': 60, 101 | 'children': [], 102 | 'successor': None 103 | } 104 | ] 105 | deserialized = from_dict(serialized) 106 | 107 | l1 = EventLabel(id='L1', name='Cause1', begin=0, end=30) 108 | l2 = EventLabel(id='L2', name='Cause2', begin=31, end=60) 109 | l1.set_successor(l2, junctor='AND') 110 | expected = [l1, l2] 111 | assert deserialized == expected 112 | 113 | @pytest.mark.unit 114 | def test_eventlabel_deserialization(): 115 | serialized = [ 116 | { 117 | 'id': 'L1', 118 | 'name': 'Cause1', 119 | 'begin': 0, 120 | 'end': 30, 121 | 'children': ['L1.1', 'L1.2'], 122 | 'successor': { 123 | 'id': 'L2', 124 | 'junctor': 'AND' 125 | } 126 | }, { 127 | 'id': 'L1.1', 128 | 'name': 'Variable', 129 | 'begin': 0, 130 | 'end': 15, 131 | 'parent': 'L1' 132 | }, { 133 | 'id': 'L1.2', 134 | 'name': 'Condition', 135 | 'begin': 16, 136 | 'end': 30, 137 | 'parent': 'L1' 138 | }, { 139 | 'id': 'L2', 140 | 'name': 'Cause2', 141 | 'begin': 31, 142 | 'end': 60, 143 | 'children': [], 144 | 'successor': { 145 | 'id': 'L3', 146 | 'junctor': 'OR' 147 | } 148 | }, { 149 | 'id': 'L3', 150 | 'name': 'Cause3', 151 | 'begin': 61, 152 | 'end': 90, 153 | 'children': [], 154 | 'successor': None 155 | }, 156 | ] 157 | deserialized = from_dict(serialized) 158 | 159 | l1 = EventLabel(id='L1', name='Cause1', begin=0, end=30) 160 | l11 = SubLabel(id='L1.1', name='Variable', begin=0, end=15) 161 | l12 = SubLabel(id='L1.2', name='Condition', begin=16, end=30) 162 | l1.add_child(l11) 163 | l1.add_child(l12) 164 | 165 | l2 = EventLabel(id='L2', name='Cause2', begin=31, end=60) 166 | l3 = EventLabel(id='L3', name='Cause3', begin=61, end=90) 167 | l1.set_successor(l2, junctor='AND') 168 | l2.set_successor(l3, junctor='OR') 169 | expected = [l1, l11, l12, l2, l3] 170 | assert deserialized == expected -------------------------------------------------------------------------------- /src/data/test.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass, field, asdict 2 | from tabulate import tabulate 3 | 4 | 5 | @dataclass 6 | class Parameter: 7 | id: str 8 | variable: str = None 9 | condition: str = None 10 | 11 | def __eq__(self, other: 'Parameter') -> bool: 12 | return self.variable == other.variable and self.condition == other.condition 13 | 14 | 15 | @dataclass 16 | class Suite: 17 | conditions: list[Parameter] = field(default_factory=list) 18 | expected: list[Parameter] = field(default_factory=list) 19 | 20 | cases: list[dict] = field(default_factory=list) 21 | 22 | def to_dict(self) -> dict: 23 | """Convert a dataclass object into a simple dictionary 24 | 25 | returns: test suite as a dictionary""" 26 | return asdict(self) 27 | 28 | def get_parameter(self, id: str) -> Parameter: 29 | candidates = [parameter for parameter in (self.conditions+self.expected) if parameter.id==id] 30 | if len(candidates) > 0: 31 | return candidates[0] 32 | return None 33 | 34 | def __eq__(self, other: 'Suite') -> bool: 35 | # maintain a map which associates the parameters of one test suite to another 36 | eq_map = {} 37 | 38 | # check that both the list of conditions and expected parameters are equal in this and the other object 39 | for param_list in ["conditions", "expected"]: 40 | if len(getattr(self, param_list)) != len(getattr(other, param_list)): 41 | return False 42 | 43 | for param in getattr(self, param_list): 44 | equivalent = [candidate for candidate in getattr(other, param_list) if candidate == param] 45 | 46 | if len(equivalent) != 1: 47 | return False 48 | else: 49 | eq_map[equivalent[0].id] = param.id 50 | 51 | # check that the list of test case configurations is equal in this and the other object 52 | if len(self.cases) != len(other.cases): 53 | return False 54 | # map the test cases of the other suite to the same ids of this test suite 55 | mapped_cases = [{eq_map[param_id]: tc[param_id] for param_id in tc} for tc in other.cases] 56 | for case in self.cases: 57 | equivalent = [candidate for candidate in mapped_cases if case == candidate] 58 | 59 | if len(equivalent) != 1: 60 | return False 61 | 62 | return True 63 | 64 | def __repr__(self): 65 | """Represent the test suite as a string table. 66 | 67 | returns: test suite as a string table that is printed via tabulated""" 68 | # arrange the header of the table containing the 'id' column, the variables of the conditions, and the variables of the expected outcomes 69 | headers = ['id'] + [param.variable for param in self.conditions] + [param.variable for param in self.expected] 70 | # define the index where the conditions end and the expected outcomes begin 71 | index_expected = len(self.conditions)+1 72 | # insert pipes (representing vertical lines) between the index column, the condition columns, and the expected outcomes columns 73 | headers[1] = '| '+headers[1] 74 | headers[index_expected] = '| '+headers[index_expected] 75 | 76 | # convert each test case into a string representing the row 77 | data = [([index+1] + get_conditions(case, self.conditions+self.expected, index_expected-1)) for index, case in enumerate(self.cases)] 78 | 79 | return tabulate(data, headers=headers) 80 | 81 | def get_conditions(configuration: dict, parameters: list[Parameter], expected_index: int) -> list[str]: 82 | """Converts a test case into a list of strings containing the condition of each parameter with the respective boolean prefix (i.e., 'not ' if that parameter is False in the given configuration. 83 | 84 | parameters: 85 | configuration -- dictionary mapping each parameter id to a boolean value 86 | parameters -- list of both input and output parameters 87 | expected_index -- number of condition parameters +1 88 | 89 | returns: list of parameter conditions in the configuration""" 90 | # concatenate the conditions of the condition parameters and the outcome parameters 91 | row = [('' if configuration[param.id] else 'not ') + param.condition for param in parameters] 92 | # add pipes in the beginnning (to represent the vertical line between the id column and all other columns) as well as between conditions and expected columns 93 | row[0] = '| '+row[0] 94 | row[expected_index] = '| '+row[expected_index] 95 | return row 96 | 97 | 98 | def from_dict(dict_suite: dict) -> Suite: 99 | """Convert a test suite into an actual Suite object. This will mainly parse the conditions and expected parameters into actual Parameter objects. 100 | 101 | parameters: 102 | dict_suite -- test suite as a dictionary 103 | 104 | returnst test suite as a Suite""" 105 | conditions = [Parameter(id=c['id'], variable=c['variable'], condition=c['condition']) for c in dict_suite['conditions']] 106 | expected = [Parameter(id=c['id'], variable=c['variable'], condition=c['condition']) for c in dict_suite['expected']] 107 | 108 | return Suite(conditions=conditions, expected=expected, cases=dict_suite['cases']) 109 | -------------------------------------------------------------------------------- /test/converters/sentencetolabels/labelingconverter/test_token_labeling.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import src.converters.sentencetolabels.labelingconverter as lc 4 | 5 | sentence = "If the red button is pressed, the system shuts down." 6 | tokens = ['', 'If', 'Ġthe', 'Ġred', 'Ġbutton', 'Ġis', 'Ġpressed', ',', 'Ġthe', 'Ġsystem', 'Ġshuts', 'Ġdown', '.', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', ''] 7 | predictions = [[[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 8 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 9 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 10 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 11 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 12 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], 13 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], 14 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 15 | [0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], 16 | [0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0], 17 | [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0], 18 | [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0], 19 | [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 20 | [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], 21 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 22 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 23 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 24 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 25 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 26 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 27 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 28 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 29 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 30 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 31 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 32 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 33 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 34 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 35 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 36 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 37 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 38 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 39 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 40 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 41 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 42 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 43 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 44 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 45 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 46 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 47 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 48 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 49 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 50 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 51 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 52 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 53 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 54 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 55 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 56 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 57 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 58 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 59 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 60 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 61 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 62 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 63 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 64 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 65 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 66 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 67 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 68 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 69 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 70 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 71 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 72 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 73 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 74 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 75 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 76 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 77 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 78 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 79 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 80 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 81 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 82 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 83 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 84 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 85 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0], 86 | [0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0]]] 87 | labels = ['notrelevant', 'Cause1', 'Cause2', 'Cause3', 'Effect1', 'Effect2', 'Effect3', 'Conjunction', 'Disjunction', 'Variable', 'Condition', 'Negation'] 88 | 89 | @pytest.mark.unit 90 | def test_token_labels(): 91 | # test that every label produced is associated to a full word without whitespaces in the sentence 92 | token_labels = lc.get_token_labeling(sentence_tokens=tokens, sentence=sentence, predictions=predictions) 93 | 94 | for tl in token_labels: 95 | assert " " not in sentence[tl.begin:tl.end] -------------------------------------------------------------------------------- /test/data/graph/test_graph_serialization.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.graph import Graph, EventNode, IntermediateNode 4 | 5 | @pytest.mark.unit 6 | def test_graph_simple(): 7 | c = EventNode(id='c1', variable='cause', condition='occurs') 8 | e = EventNode(id='e1', variable='event', condition='happens') 9 | edge = e.add_incoming(c) 10 | graph = Graph(nodes=[c, e], root=c, edges=[edge]) 11 | serialized = graph.to_dict() 12 | 13 | expected = { 14 | 'nodes': [ 15 | {'id': 'c1', 'variable': 'cause', 'condition': 'occurs'}, 16 | {'id': 'e1', 'variable': 'event', 'condition': 'happens'} 17 | ], 18 | 'root': 'c1', 19 | 'edges': [ 20 | {'origin': 'c1', 'target': 'e1', 'negated': False} 21 | ] 22 | } 23 | assert serialized == expected 24 | 25 | @pytest.mark.unit 26 | def test_graph_negation(): 27 | c = EventNode(id='c1', variable='cause', condition='occurs') 28 | e = EventNode(id='e1', variable='event', condition='happens') 29 | edge = e.add_incoming(c, negated=True) 30 | graph = Graph(nodes=[c, e], root=c, edges=[edge]) 31 | serialized = graph.to_dict() 32 | 33 | expected = { 34 | 'nodes': [ 35 | {'id': 'c1', 'variable': 'cause', 'condition': 'occurs'}, 36 | {'id': 'e1', 'variable': 'event', 'condition': 'happens'} 37 | ], 38 | 'root': 'c1', 39 | 'edges': [ 40 | {'origin': 'c1', 'target': 'e1', 'negated': True} 41 | ] 42 | } 43 | assert serialized == expected 44 | 45 | @pytest.mark.unit 46 | def test_graph_multicause(): 47 | c1 = EventNode(id='c1', variable='cause1', condition='occurs') 48 | c2 = EventNode(id='c2', variable='cause2', condition='occurs') 49 | i = IntermediateNode(id='i1', conjunction=False) 50 | e = EventNode(id='e1', variable='event', condition='happens') 51 | 52 | edges = [] 53 | edges.append(i.add_incoming(c1)) 54 | edges.append(i.add_incoming(c2)) 55 | edges.append(e.add_incoming(i)) 56 | graph = Graph(nodes=[c1, c2, i, e], root=i, edges=edges) 57 | serialized = graph.to_dict() 58 | 59 | expected = { 60 | 'nodes': [ 61 | {'id': 'c1', 'variable': 'cause1', 'condition': 'occurs'}, 62 | {'id': 'c2', 'variable': 'cause2', 'condition': 'occurs'}, 63 | {'id': 'i1', 'conjunction': False, 'precedence': False}, 64 | {'id': 'e1', 'variable': 'event', 'condition': 'happens'} 65 | ], 66 | 'root': 'i1', 67 | 'edges': [ 68 | {'origin': 'c1', 'target': 'i1', 'negated': False}, 69 | {'origin': 'c2', 'target': 'i1', 'negated': False}, 70 | {'origin': 'i1', 'target': 'e1', 'negated': False} 71 | ] 72 | } 73 | assert serialized == expected 74 | 75 | @pytest.mark.unit 76 | def test_graph_multieffect(): 77 | c = EventNode(id='c1', variable='cause', condition='occurs') 78 | e1 = EventNode(id='e1', variable='event1', condition='happens') 79 | e2 = EventNode(id='e2', variable='event2', condition='happens') 80 | 81 | edges = [] 82 | edges.append(e1.add_incoming(c)) 83 | edges.append(e2.add_incoming(c)) 84 | graph = Graph(nodes=[c, e1, e2], root=c, edges=edges) 85 | serialized = graph.to_dict() 86 | 87 | expected = { 88 | 'nodes': [ 89 | {'id': 'c1', 'variable': 'cause', 'condition': 'occurs'}, 90 | {'id': 'e1', 'variable': 'event1', 'condition': 'happens'}, 91 | {'id': 'e2', 'variable': 'event2', 'condition': 'happens'} 92 | ], 93 | 'root': 'c1', 94 | 'edges': [ 95 | {'origin': 'c1', 'target': 'e1', 'negated': False}, 96 | {'origin': 'c1', 'target': 'e2', 'negated': False} 97 | ] 98 | } 99 | assert serialized == expected 100 | 101 | @pytest.mark.unit 102 | def test_graph_multi(): 103 | c1 = EventNode(id='c1', variable='cause1', condition='occurs') 104 | c2 = EventNode(id='c2', variable='cause2', condition='occurs') 105 | i = IntermediateNode(id='i1', conjunction=False) 106 | e1 = EventNode(id='e1', variable='event1', condition='happens') 107 | e2 = EventNode(id='e2', variable='event2', condition='happens') 108 | 109 | edges = [] 110 | edges.append(i.add_incoming(c1, negated=True)) 111 | edges.append(i.add_incoming(c2)) 112 | edges.append(e1.add_incoming(i)) 113 | edges.append(e2.add_incoming(i, negated=True)) 114 | graph = Graph(nodes=[c1, c2, i, e1, e2], root=i, edges=edges) 115 | serialized = graph.to_dict() 116 | 117 | expected = { 118 | 'nodes': [ 119 | {'id': 'c1', 'variable': 'cause1', 'condition': 'occurs'}, 120 | {'id': 'c2', 'variable': 'cause2', 'condition': 'occurs'}, 121 | {'id': 'i1', 'conjunction': False, 'precedence': False}, 122 | {'id': 'e1', 'variable': 'event1', 'condition': 'happens'}, 123 | {'id': 'e2', 'variable': 'event2', 'condition': 'happens'} 124 | ], 125 | 'root': 'i1', 126 | 'edges': [ 127 | {'origin': 'c1', 'target': 'i1', 'negated': True}, 128 | {'origin': 'c2', 'target': 'i1', 'negated': False}, 129 | {'origin': 'i1', 'target': 'e1', 'negated': False}, 130 | {'origin': 'i1', 'target': 'e2', 'negated': True} 131 | ] 132 | } 133 | assert serialized == expected -------------------------------------------------------------------------------- /test/data/graph/test_graph_deserialization.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.graph import Graph, EventNode, IntermediateNode, from_dict 4 | 5 | @pytest.mark.unit 6 | def test_graph(): 7 | serialized = { 8 | 'nodes': [ 9 | {'id': 'c1', 'variable': 'cause', 'condition': 'occurs'}, 10 | {'id': 'e1', 'variable': 'event', 'condition': 'happens'} 11 | ], 12 | 'root': 'c1', 13 | 'edges': [ 14 | {'origin': 'c1', 'target': 'e1', 'negated': False} 15 | ] 16 | } 17 | deserialized = from_dict(serialized) 18 | 19 | c = EventNode(id='c1', variable='cause', condition='occurs') 20 | e = EventNode(id='e1', variable='event', condition='happens') 21 | edge = e.add_incoming(c) 22 | expected = Graph(nodes=[c, e], root=c, edges=[edge]) 23 | 24 | assert deserialized == expected 25 | 26 | @pytest.mark.unit 27 | def test_graph_negation(): 28 | serialized = { 29 | 'nodes': [ 30 | {'id': 'c1', 'variable': 'cause', 'condition': 'occurs'}, 31 | {'id': 'e1', 'variable': 'event', 'condition': 'happens'} 32 | ], 33 | 'root': 'c1', 34 | 'edges': [ 35 | {'origin': 'c1', 'target': 'e1', 'negated': True} 36 | ] 37 | } 38 | deserialized = from_dict(serialized) 39 | 40 | 41 | c = EventNode(id='c1', variable='cause', condition='occurs') 42 | e = EventNode(id='e1', variable='event', condition='happens') 43 | edge = e.add_incoming(c, negated=True) 44 | expected = Graph(nodes=[c, e], root=c, edges=[edge]) 45 | 46 | assert deserialized == expected 47 | 48 | @pytest.mark.unit 49 | def test_graph_multicause(): 50 | serialized = { 51 | 'nodes': [ 52 | {'id': 'c1', 'variable': 'cause1', 'condition': 'occurs'}, 53 | {'id': 'c2', 'variable': 'cause2', 'condition': 'occurs'}, 54 | {'id': 'i1', 'conjunction': False}, 55 | {'id': 'e1', 'variable': 'event', 'condition': 'happens'} 56 | ], 57 | 'root': 'i1', 58 | 'edges': [ 59 | {'origin': 'c1', 'target': 'i1', 'negated': False}, 60 | {'origin': 'c2', 'target': 'i1', 'negated': False}, 61 | {'origin': 'i1', 'target': 'e1', 'negated': False} 62 | ] 63 | } 64 | deserialized = from_dict(serialized) 65 | 66 | c1 = EventNode(id='c1', variable='cause1', condition='occurs') 67 | c2 = EventNode(id='c2', variable='cause2', condition='occurs') 68 | i = IntermediateNode(id='i1', conjunction=False) 69 | e = EventNode(id='e1', variable='event', condition='happens') 70 | 71 | edges = [] 72 | edges.append(i.add_incoming(c1)) 73 | edges.append(i.add_incoming(c2)) 74 | edges.append(e.add_incoming(i)) 75 | expected = Graph(nodes=[c1, c2, i, e], root=i, edges=edges) 76 | 77 | assert deserialized == expected 78 | 79 | @pytest.mark.unit 80 | def test_graph_multieffect(): 81 | serialized = { 82 | 'nodes': [ 83 | {'id': 'c1', 'variable': 'cause', 'condition': 'occurs'}, 84 | {'id': 'e1', 'variable': 'event1', 'condition': 'happens'}, 85 | {'id': 'e2', 'variable': 'event2', 'condition': 'happens'} 86 | ], 87 | 'root': 'c1', 88 | 'edges': [ 89 | {'origin': 'c1', 'target': 'e1', 'negated': False}, 90 | {'origin': 'c1', 'target': 'e2', 'negated': False} 91 | ] 92 | } 93 | deserialized = from_dict(serialized) 94 | 95 | c = EventNode(id='c1', variable='cause', condition='occurs') 96 | e1 = EventNode(id='e1', variable='event1', condition='happens') 97 | e2 = EventNode(id='e2', variable='event2', condition='happens') 98 | 99 | edges = [] 100 | edges.append(e1.add_incoming(c)) 101 | edges.append(e2.add_incoming(c)) 102 | expected = Graph(nodes=[c, e1, e2], root=c, edges=edges) 103 | 104 | assert deserialized == expected 105 | 106 | @pytest.mark.unit 107 | def test_graph_multi(): 108 | serialized = { 109 | 'nodes': [ 110 | {'id': 'c1', 'variable': 'cause1', 'condition': 'occurs'}, 111 | {'id': 'c2', 'variable': 'cause2', 'condition': 'occurs'}, 112 | {'id': 'i1', 'conjunction': False}, 113 | {'id': 'e1', 'variable': 'event1', 'condition': 'happens'}, 114 | {'id': 'e2', 'variable': 'event2', 'condition': 'happens'} 115 | ], 116 | 'root': 'i1', 117 | 'edges': [ 118 | {'origin': 'c1', 'target': 'i1', 'negated': True}, 119 | {'origin': 'c2', 'target': 'i1', 'negated': False}, 120 | {'origin': 'i1', 'target': 'e1', 'negated': False}, 121 | {'origin': 'i1', 'target': 'e2', 'negated': True} 122 | ] 123 | } 124 | deserialized = from_dict(serialized) 125 | 126 | c1 = EventNode(id='c1', variable='cause1', condition='occurs') 127 | c2 = EventNode(id='c2', variable='cause2', condition='occurs') 128 | i = IntermediateNode(id='i1', conjunction=False) 129 | e1 = EventNode(id='e1', variable='event1', condition='happens') 130 | e2 = EventNode(id='e2', variable='event2', condition='happens') 131 | 132 | edges = [] 133 | edges.append(i.add_incoming(c1, negated=True)) 134 | edges.append(i.add_incoming(c2)) 135 | edges.append(e1.add_incoming(i)) 136 | edges.append(e2.add_incoming(i, negated=True)) 137 | expected = Graph(nodes=[c1, c2, i, e1, e2], root=i, edges=edges) 138 | 139 | assert deserialized == expected -------------------------------------------------------------------------------- /test/data/graph/Graph/test_graph_eq.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from src.data.graph import Graph, EventNode, IntermediateNode 4 | 5 | @pytest.mark.integration 6 | def test_simple(): 7 | edgelist = [] 8 | event1 = EventNode(id='E1', variable='the red button', condition = 'is pushed') 9 | event2 = EventNode(id='E2', variable = 'the blue button', condition = 'is released') 10 | 11 | junctor = IntermediateNode(id='E3', conjunction=False) 12 | edgelist.append(junctor.add_incoming(child=event1, negated=False)) 13 | edgelist.append(junctor.add_incoming(child=event2, negated=False)) 14 | 15 | event3 = EventNode(id='E3', variable = 'the system', condition = 'will shut down') 16 | edgelist.append(event3.add_incoming(child=junctor, negated=False)) 17 | 18 | graph1 = Graph(nodes=[event1, event2, junctor, event3], root=junctor, edges=edgelist) 19 | assert graph1 == graph1 20 | 21 | @pytest.mark.integration 22 | def test_switched_node_order(): 23 | edgelist1 = [] 24 | event1 = EventNode(id='E1', variable = 'the red button', condition = 'is pushed') 25 | event2 = EventNode(id='E2', variable = 'the blue button', condition = 'is released') 26 | 27 | junctor1 = IntermediateNode(id='E3', conjunction=False) 28 | edgelist1.append(junctor1.add_incoming(child=event1, negated=False)) 29 | edgelist1.append(junctor1.add_incoming(child=event2, negated=False)) 30 | 31 | event3 = EventNode(id='E3', variable = 'the system', condition = 'will shut down') 32 | edgelist1.append(event3.add_incoming(child=junctor1, negated=False)) 33 | 34 | graph1 = Graph(nodes=[event1, event2, junctor1, event3], root=junctor1, edges=edgelist1) 35 | 36 | edgelist2 = [] 37 | event21 = EventNode(id='E1', variable = 'the red button', condition = 'is pushed') 38 | event22 = EventNode(id='E2', variable = 'the blue button', condition = 'is released') 39 | 40 | junctor2 = IntermediateNode(id='E3', conjunction=False) 41 | edgelist2.append(junctor2.add_incoming(child=event22, negated=False)) 42 | edgelist2.append(junctor2.add_incoming(child=event21, negated=False)) 43 | 44 | event23 = EventNode(id='E3', variable = 'the system', condition = 'will shut down') 45 | edgelist2.append(event23.add_incoming(child=junctor2, negated=False)) 46 | 47 | graph2 = Graph(nodes=[event21, event22, junctor2, event23], root=junctor2, edges=edgelist2) 48 | assert graph1 == graph2 49 | 50 | @pytest.mark.integration 51 | def test_inequal_number_of_effects(): 52 | # graph 1 53 | root = EventNode(id='c1', labels=None, variable='an error', condition='occurs') 54 | effect1 = EventNode(id='e1', variable='a message', condition='is shown') 55 | effect1.add_incoming(child=root, negated=False) 56 | graph1 = Graph(nodes=[root, effect1], root=root, edges=None) 57 | 58 | # graph 2 59 | root = EventNode(id='c1', labels=None, variable='an error', condition='occurs') 60 | effect1 = EventNode(id='e1', variable='a message', condition='is shown') 61 | effect2 = EventNode(id='e2', variable='a sound', condition='is played') 62 | effect1.add_incoming(child=root) 63 | effect2.add_incoming(child=root) 64 | graph2 = Graph(nodes=[root, effect1, effect2], root=root, edges=None) 65 | 66 | assert graph1 != graph2 67 | 68 | @pytest.mark.integration 69 | def test_inequal_effects_variable(): 70 | # graph 1 71 | root1 = EventNode(id='c1', labels=None, variable='an error', condition='occurs') 72 | effect1 = EventNode(id='e1', variable='a message', condition='is shown') 73 | effect1.add_incoming(child=root1, negated=False) 74 | graph1 = Graph(nodes=[root1, effect1], root=root1, edges=None) 75 | 76 | # graph 2: the variable of the effect node is different 77 | root2 = EventNode(id='c1', labels=None, variable='an error', condition='occurs') 78 | effect2 = EventNode(id='e1', variable='the message', condition='is shown') 79 | effect2.add_incoming(child=root2) 80 | graph2 = Graph(nodes=[root2, effect1], root=root2, edges=None) 81 | 82 | assert graph1 != graph2 83 | 84 | @pytest.mark.integration 85 | def test_inequal_effects_condition(): 86 | # graph 1 87 | root1 = EventNode(id='c1', labels=None, variable='an error', condition='occurs') 88 | effect1 = EventNode(id='e1', variable='a message', condition='is shown') 89 | effect1.add_incoming(child=root1, negated=False) 90 | graph1 = Graph(nodes=[root1, effect1], root=root1, edges=None) 91 | 92 | # graph 2: the variable of the effect node is different 93 | root2 = EventNode(id='c1', labels=None, variable='an error', condition='occurs') 94 | effect2 = EventNode(id='e1', variable='a message', condition='is opened') 95 | effect2.add_incoming(child=root2) 96 | graph2 = Graph(nodes=[root2, effect1], root=root2, edges=None) 97 | 98 | assert graph1 != graph2 99 | 100 | @pytest.mark.integration 101 | def test_inequal_effects_negation(): 102 | # graph 1 103 | root1 = EventNode(id='c1', labels=None, variable='an error', condition='occurs') 104 | effect1 = EventNode(id='e1', variable='a message', condition='is shown') 105 | effect1.add_incoming(child=root1, negated=False) 106 | graph1 = Graph(nodes=[root1, effect1], root=root1, edges=None) 107 | 108 | # graph 2: the variable of the effect node is different 109 | root2 = EventNode(id='c1', labels=None, variable='an error', condition='occurs') 110 | effect2 = EventNode(id='e1', variable='a message', condition='is shown') 111 | effect2.add_incoming(child=root2, negated=True) 112 | graph2 = Graph(nodes=[root2, effect1], root=root2, edges=None) 113 | 114 | assert graph1 != graph2 -------------------------------------------------------------------------------- /src/api/service.py: -------------------------------------------------------------------------------- 1 | from abc import abstractmethod 2 | 3 | from src.cira import CiRAConverter 4 | 5 | from src.data.labels import Label 6 | from src.data.labels import from_dict as labels_from_dict 7 | 8 | from src.data.graph import Graph 9 | from src.data.graph import from_dict as graph_from_dict 10 | 11 | from src.data.test import Suite 12 | 13 | 14 | class CiRAService: 15 | @abstractmethod 16 | def classify(self, sentence: str) -> tuple[bool, float]: 17 | pass 18 | 19 | @abstractmethod 20 | def sentence_to_labels(self, sentence: str) -> list[dict]: 21 | pass 22 | 23 | @abstractmethod 24 | def sentence_to_graph(self, sentence: str, labels: list) -> dict: 25 | pass 26 | 27 | @abstractmethod 28 | def graph_to_test(self, graph, sentence: str) -> dict: 29 | pass 30 | 31 | 32 | class CiRAServiceImpl(CiRAService): 33 | 34 | def __init__(self, model_classification: str, model_labeling: str, use_GPU: bool = False): 35 | self.cira = CiRAConverter( 36 | classifier_causal_model_path=model_classification, 37 | converter_s2l_model_path=model_labeling, 38 | use_GPU=use_GPU) 39 | 40 | def classify(self, sentence: str) -> tuple[bool, float]: 41 | """Classify a given sentence as either causal or non-causal. 42 | 43 | parameters: 44 | sentence -- single natural language sentence 45 | 46 | returns: 47 | causal -- True, if the sentence is considered to be causal 48 | confidence -- float value between 0 and 1 representing the confidence with which the classified chose either label""" 49 | causal, confidence = self.cira.classify(sentence) 50 | return causal, confidence 51 | 52 | def sentence_to_labels(self, sentence: str) -> list[dict]: 53 | """Generate the causal labels for a sentence. 54 | 55 | parameters: 56 | sentence -- single natural language sentence 57 | 58 | returns: list of labels serialized to dictionaries 59 | """ 60 | labels: list[Label] = self.cira.label(sentence) 61 | 62 | labels_serialized: list[dict] = [label.to_dict() for label in labels] 63 | return labels_serialized 64 | 65 | def sentence_to_graph(self, sentence: str, labels: list) -> dict: 66 | """Generate a cause-effect-graph from a sentence and a list of labels. If the labels are not given, they will be generated. 67 | 68 | parameters: 69 | sentence -- single natural language sentence 70 | labels -- list of labels (either as true labels or dictionaries) 71 | 72 | returns: graph serialized to a dictionary 73 | """ 74 | labels_deserialized: list[Label] = self.get_deserialized_labels(sentence, labels) 75 | 76 | graph: Graph = self.cira.graph(sentence, labels_deserialized) 77 | 78 | graph_serialized: dict = graph.to_dict() 79 | return graph_serialized 80 | 81 | def get_deserialized_labels(self, sentence: str, labels: list) -> list[Label]: 82 | """Recovers a list of labels and ensures that it is in the right format. This includes (1) generating new labels if the current list is None or empty and (2) casting labels serialized to dictionaries back to actual labels. 83 | 84 | parameters: 85 | sentence -- single, causal, natural language sentence 86 | labels -- list of labels 87 | 88 | returns: list of actual labels representing the causal relationship implied by the sentence""" 89 | labels_missing = (labels is None) or (len(labels) == 0) 90 | if labels_missing: 91 | return self.cira.label(sentence) 92 | 93 | labels_serialized = (type(labels[0]) == dict) 94 | if labels_serialized: 95 | return labels_from_dict(labels) 96 | 97 | return labels 98 | 99 | def graph_to_test(self, graph, sentence: str) -> dict: 100 | """Generate a test suite from a cause-effect graph. 101 | 102 | parameters: 103 | graph -- a cause effect graph (either as a true Graph or a dictionary) 104 | 105 | returns: test suite serialized to a dictionary 106 | """ 107 | if not graph: 108 | graph = self.sentence_to_graph(sentence, labels=[]) 109 | 110 | graph_serialized = (type(graph) == dict) 111 | if graph_serialized: 112 | graph: Graph = graph_from_dict(graph) 113 | 114 | suite: Suite = self.cira.testsuite(ceg=graph) 115 | 116 | suite_serialized: dict = suite.to_dict() 117 | return suite_serialized 118 | 119 | 120 | class CiraServiceMock(CiRAService): 121 | 122 | def __init__(self): 123 | pass 124 | 125 | def classify(self, sentence) -> tuple[bool, float]: 126 | return (True, 0.99) 127 | 128 | def sentence_to_labels(self, sentence: str) -> list[dict]: 129 | return [{'id': 'L1', 'name': 'Variable', 'begin': 10, 'end': 20, 'parent': None}] 130 | 131 | def sentence_to_graph(self, sentence: str, labels: list) -> dict: 132 | return { 133 | 'nodes': [ 134 | {'id': 'c', 'variable': 'the button', 'condition': 'is pressed'}, 135 | {'id': 'e', 'variable': 'the system', 'condition': 'shuts down'} 136 | ], 137 | 'root': 'c', 138 | 'edges': [{'origin': 'c', 'target': 'e', 'negated': False}] 139 | } 140 | 141 | def graph_to_test(self, graph, sentence: str) -> dict: 142 | return { 143 | 'conditions': [{'id': 'c', 'variable': 'the button', 'condition': 'is pressed'}], 144 | 'expected': [{'id': 'c', 'variable': 'the system', 'condition': 'shuts down'}], 145 | 'cases': [{'c': True, 'e': True}, {'c': False, 'e': False}] 146 | } 147 | -------------------------------------------------------------------------------- /static/sentences/sentence-12.json: -------------------------------------------------------------------------------- 1 | { 2 | "sentence": "When the red button is pushed or the power fails the interface does not accept further inputs and the system shuts down.", 3 | "labels": [ 4 | { 5 | "id": "T2", 6 | "name": "Cause1", 7 | "begin": 5, 8 | "end": 29, 9 | "successor": { 10 | "id": "T6", 11 | "junctor": "OR" 12 | }, 13 | "children": [ 14 | "T3", 15 | "T4" 16 | ] 17 | }, 18 | { 19 | "id": "T3", 20 | "name": "Variable", 21 | "begin": 5, 22 | "end": 19, 23 | "parent": "T2" 24 | }, 25 | { 26 | "id": "T4", 27 | "name": "Condition", 28 | "begin": 20, 29 | "end": 29, 30 | "parent": "T2" 31 | }, 32 | { 33 | "id": "T5", 34 | "name": "Disjunction", 35 | "begin": 30, 36 | "end": 32, 37 | "parent": null 38 | }, 39 | { 40 | "id": "T6", 41 | "name": "Cause2", 42 | "begin": 33, 43 | "end": 48, 44 | "successor": { 45 | "id": "T9", 46 | "junctor": null 47 | }, 48 | "children": [ 49 | "T7", 50 | "T8" 51 | ] 52 | }, 53 | { 54 | "id": "T7", 55 | "name": "Variable", 56 | "begin": 33, 57 | "end": 42, 58 | "parent": "T6" 59 | }, 60 | { 61 | "id": "T8", 62 | "name": "Condition", 63 | "begin": 43, 64 | "end": 48, 65 | "parent": "T6" 66 | }, 67 | { 68 | "id": "T9", 69 | "name": "Effect1", 70 | "begin": 49, 71 | "end": 93, 72 | "successor": { 73 | "id": "T14", 74 | "junctor": "AND" 75 | }, 76 | "children": [ 77 | "T10", 78 | "T11", 79 | "T12" 80 | ] 81 | }, 82 | { 83 | "id": "T10", 84 | "name": "Variable", 85 | "begin": 49, 86 | "end": 62, 87 | "parent": "T9" 88 | }, 89 | { 90 | "id": "T11", 91 | "name": "Negation", 92 | "begin": 63, 93 | "end": 71, 94 | "parent": "T9" 95 | }, 96 | { 97 | "id": "T12", 98 | "name": "Condition", 99 | "begin": 72, 100 | "end": 93, 101 | "parent": "T9" 102 | }, 103 | { 104 | "id": "T13", 105 | "name": "Conjunction", 106 | "begin": 94, 107 | "end": 97, 108 | "parent": null 109 | }, 110 | { 111 | "id": "T14", 112 | "name": "Effect2", 113 | "begin": 98, 114 | "end": 119, 115 | "successor": null, 116 | "children": [ 117 | "T15", 118 | "T16" 119 | ] 120 | }, 121 | { 122 | "id": "T15", 123 | "name": "Variable", 124 | "begin": 98, 125 | "end": 108, 126 | "parent": "T14" 127 | }, 128 | { 129 | "id": "T16", 130 | "name": "Condition", 131 | "begin": 109, 132 | "end": 119, 133 | "parent": "T14" 134 | } 135 | ], 136 | "graph": { 137 | "nodes": [ 138 | { 139 | "id": "N1", 140 | "variable": "the red button", 141 | "condition": "is pushed" 142 | }, 143 | { 144 | "id": "N2", 145 | "variable": "the power", 146 | "condition": "fails" 147 | }, 148 | { 149 | "id": "N3", 150 | "conjunction": false 151 | }, 152 | { 153 | "id": "N4", 154 | "variable": "the interface", 155 | "condition": "accept further inputs" 156 | }, 157 | { 158 | "id": "N5", 159 | "variable": "the system", 160 | "condition": "shuts down" 161 | } 162 | ], 163 | "root": "N3", 164 | "edges": [ 165 | { 166 | "origin": "N1", 167 | "target": "N3", 168 | "negated": false 169 | }, 170 | { 171 | "origin": "N2", 172 | "target": "N3", 173 | "negated": false 174 | }, 175 | { 176 | "origin": "N3", 177 | "target": "N4", 178 | "negated": true 179 | }, 180 | { 181 | "origin": "N3", 182 | "target": "N5", 183 | "negated": false 184 | } 185 | ] 186 | }, 187 | "testsuite": { 188 | "conditions": [ 189 | { 190 | "id": "P1", 191 | "variable": "the red button", 192 | "condition": "is pushed" 193 | }, 194 | { 195 | "id": "P2", 196 | "variable": "the power", 197 | "condition": "fails" 198 | } 199 | ], 200 | "expected": [ 201 | { 202 | "id": "P3", 203 | "variable": "the interface", 204 | "condition": "accept further inputs" 205 | }, 206 | { 207 | "id": "P4", 208 | "variable": "the system", 209 | "condition": "shuts down" 210 | } 211 | ], 212 | "cases": [ 213 | { 214 | "P1": false, 215 | "P2": false, 216 | "P3": true, 217 | "P4": false 218 | }, 219 | { 220 | "P1": true, 221 | "P2": false, 222 | "P3": false, 223 | "P4": true 224 | }, 225 | { 226 | "P1": false, 227 | "P2": true, 228 | "P3": false, 229 | "P4": true 230 | } 231 | ] 232 | } 233 | } --------------------------------------------------------------------------------