├── scripts ├── fix_code_style.sh └── check_code_style.sh ├── .gitignore ├── src └── vpselector │ ├── __init__.py │ ├── windows │ ├── __init__.py │ ├── confirm_selection_window.py │ └── main_window.py │ ├── widgets │ ├── __init__.py │ ├── dataframe_plot_widget.py │ ├── mpl_widget.py │ ├── histogram_plot_widget.py │ └── time_series_data_plot_widget.py │ └── data_selection.py ├── NOTICE.txt ├── .github └── workflows │ └── check_style.yaml ├── vpselector_example.py ├── pyproject.toml ├── README.md └── LICENSE.txt /scripts/fix_code_style.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | black . -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv/ 2 | .vscode/ 3 | __pycache__/ 4 | /*.csv 5 | build/ 6 | *.egg-info/ 7 | dist/ -------------------------------------------------------------------------------- /src/vpselector/__init__.py: -------------------------------------------------------------------------------- 1 | from . import widgets 2 | from . import windows 3 | from .data_selection import select_visual_data, select_visual_data_in_pyqt_app 4 | -------------------------------------------------------------------------------- /src/vpselector/windows/__init__.py: -------------------------------------------------------------------------------- 1 | from .confirm_selection_window import ConfirmSelectionWindow 2 | from . import main_window 3 | from .main_window import MainWindow -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Visual Pandas Cropper 2 | Copyright 2016 The Apache Software Foundation. 3 | 4 | This software was developed by Manuel Yves Galliker (@manumerous, manuel.galliker@gmx.ch) 5 | -------------------------------------------------------------------------------- /src/vpselector/widgets/__init__.py: -------------------------------------------------------------------------------- 1 | from .mpl_widget import MplWidget 2 | from .dataframe_plot_widget import DataFramePlotWidget 3 | from .time_series_data_plot_widget import TimeSeriesDataPlotWidget 4 | from .histogram_plot_widget import HistogramPlotWidget -------------------------------------------------------------------------------- /scripts/check_code_style.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Fix style recursively in all the repository 4 | sh scripts/fix_code_style.sh . 5 | 6 | # Print the diff with the remote branch (empty if no diff) 7 | git --no-pager diff -U0 --color 8 | 9 | # Check if there are changes, and failed 10 | if ! git diff-index --quiet HEAD --; then echo "Code style check failed, please run scripts/fix_code_style.sh"; exit 1; fi 11 | -------------------------------------------------------------------------------- /.github/workflows/check_style.yaml: -------------------------------------------------------------------------------- 1 | name: Style Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - '*' 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: Install requirements 17 | run: pip install . 18 | - name: Check Code format 19 | run: | 20 | ./scripts/check_code_style.sh . 21 | shell: bash 22 | -------------------------------------------------------------------------------- /src/vpselector/windows/confirm_selection_window.py: -------------------------------------------------------------------------------- 1 | __author__ = "Manuel Yves Galliker" 2 | __maintainer__ = "Manuel Yves Galliker" 3 | __license__ = "Apache-2.0" 4 | 5 | from PyQt5 import QtCore 6 | from PyQt5.QtWidgets import QVBoxLayout, QWidget, QDialog, QDialogButtonBox 7 | 8 | class ConfirmSelectionWindow(QDialog): 9 | def __init__(self): 10 | super().__init__() 11 | 12 | buttonBox = QDialogButtonBox(QDialogButtonBox.Cancel | QDialogButtonBox.Ok) 13 | 14 | buttonBox.accepted.connect(self.accept) 15 | buttonBox.rejected.connect(self.reject) 16 | 17 | layout = QVBoxLayout() 18 | 19 | layout.addWidget(buttonBox) 20 | 21 | # Set dialog layout 22 | self.setLayout(layout) 23 | self.setWindowTitle("Confirm selection") 24 | self.setWindowModality(QtCore.Qt.ApplicationModal) 25 | -------------------------------------------------------------------------------- /vpselector_example.py: -------------------------------------------------------------------------------- 1 | __author__ = "Manuel Yves Galliker" 2 | __maintainer__ = "Manuel Yves Galliker" 3 | __license__ = "Apache-2.0" 4 | 5 | import pandas as pd 6 | from pathlib import Path 7 | 8 | 9 | import vpselector 10 | 11 | def test_vpselector(): 12 | test_file_path = str(Path(__file__).parent.absolute()) + "/test/test.csv" 13 | print(test_file_path) 14 | plot_config_dict = { 15 | "x_axis_col": "timestamp", 16 | "sub_plt1_data": ["q0", "q1", "q2", "q3"], 17 | "sub_plt2_data": ["u0", "u1", "u2", "u3", "u4", "u5", "u6", "u7"], 18 | } 19 | data_df = pd.read_csv(test_file_path, index_col=0) 20 | 21 | selected_df = vpselector.data_selection.select_visual_data(data_df, plot_config_dict) 22 | print("Selected dataframe:") 23 | print(selected_df) 24 | return 25 | 26 | 27 | if __name__ == "__main__": 28 | test_vpselector() 29 | -------------------------------------------------------------------------------- /src/vpselector/widgets/dataframe_plot_widget.py: -------------------------------------------------------------------------------- 1 | __author__ = "Manuel Yves Galliker" 2 | __maintainer__ = "Manuel Yves Galliker" 3 | __license__ = "Apache-2.0" 4 | 5 | 6 | from vpselector.widgets.mpl_widget import MplWidget 7 | 8 | from abc import abstractmethod 9 | import pandas as pd 10 | import copy 11 | 12 | 13 | class DataFramePlotWidget(MplWidget): 14 | def __init__(self, plot_config : dict, parentWindow): 15 | self.plot_config = copy.deepcopy(plot_config) 16 | self.x_axis_col = self.plot_config["x_axis_col"] 17 | self.plot_config.pop("x_axis_col", None) 18 | 19 | self.subplot_keys = list(self.plot_config.keys()) 20 | self.subplot_count = len(self.subplot_keys) 21 | 22 | super(DataFramePlotWidget, self).__init__(parentWindow, self.subplot_count) 23 | 24 | @abstractmethod 25 | def plot(self, df: pd.DataFrame): 26 | """Visualize the data from df in each subplot according to the specified columns in self.plot_config.""" 27 | pass 28 | -------------------------------------------------------------------------------- /src/vpselector/data_selection.py: -------------------------------------------------------------------------------- 1 | __author__ = "Manuel Yves Galliker" 2 | __maintainer__ = "Manuel Yves Galliker" 3 | __license__ = "Apache-2.0" 4 | 5 | 6 | import pandas as pd 7 | from pandas.core.indexes.base import Index 8 | from PyQt5 import QtCore, QtGui, QtWidgets 9 | import sys 10 | 11 | from vpselector.windows.main_window import MainWindow 12 | 13 | # use this function if no pyqt app is running 14 | def select_visual_data(data : pd.DataFrame , plot_config : dict): 15 | app = QtWidgets.QApplication(sys.argv) 16 | w = MainWindow(data, plot_config) 17 | app.exec_() 18 | return w.cropped_data 19 | 20 | # use this function to include the visual data selection in a running pyqt app 21 | def select_visual_data_in_pyqt_app(data, plot_config : dict, pyqt_app): 22 | w = MainWindow(data, plot_config) 23 | pyqt_app.processEvents() # Process events once before waiting 24 | while w.isVisible(): # Loop until window is closed 25 | pyqt_app.processEvents() 26 | return w.cropped_data 27 | -------------------------------------------------------------------------------- /src/vpselector/widgets/mpl_widget.py: -------------------------------------------------------------------------------- 1 | __author__ = "Manuel Yves Galliker" 2 | __maintainer__ = "Manuel Yves Galliker" 3 | __license__ = "Apache-2.0" 4 | 5 | from matplotlib.figure import Figure 6 | from matplotlib.backends.backend_qt5agg import FigureCanvas 7 | from PyQt5.QtWidgets import QVBoxLayout, QWidget 8 | 9 | 10 | class MplWidget(QWidget): 11 | def __init__(self, parentWindow=None, subplot_count=1): 12 | QWidget.__init__(self, parentWindow) 13 | 14 | self.canvas = FigureCanvas(Figure()) 15 | 16 | vertical_layout = QVBoxLayout() 17 | vertical_layout.addWidget(self.canvas) 18 | 19 | self.canvas.subplot_axes = [] 20 | 21 | for i in range(subplot_count): 22 | if i == 0: 23 | ax1 = self.canvas.figure.add_subplot(subplot_count, 1, i + 1) 24 | self.canvas.subplot_axes.append(ax1) 25 | else: 26 | curr_ax = self.canvas.figure.add_subplot( 27 | subplot_count, 1, i + 1, sharex=ax1 28 | ) 29 | self.canvas.subplot_axes.append(curr_ax) 30 | 31 | self.setLayout(vertical_layout) 32 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "vpselector" 3 | version = "1.0.2" 4 | description = "Visualize and interactively select time-series data from a pandas DataFrame." 5 | readme = "README.md" 6 | requires-python = ">=3.8" 7 | license = { file = "LICENSE.txt" } 8 | keywords = [ 9 | "python", 10 | "dataframe", 11 | "pandas", 12 | "visualization", 13 | "data selection", 14 | "time-series data", 15 | "data tools", 16 | "data science", 17 | "data-driven engineering", 18 | "machine learning", 19 | "system identification", 20 | ] 21 | authors = [ 22 | { email = "manuel@galliker.tech" }, 23 | { name = "Manuel Yves Galliker" }, 24 | ] 25 | maintainers = [ 26 | { name = "Manuel Yves Galliker", email = "manuel@galliker.tech" }, 27 | ] 28 | classifiers = [ 29 | "Development Status :: 4 - Beta", 30 | "Programming Language :: Python", 31 | ] 32 | 33 | dependencies = [ 34 | "pandas >= 1.1.5", 35 | "PyQt5>=5.15.0", 36 | "matplotlib>=3.7.1", 37 | "seaborn>=0.11.1", 38 | "overrides>=7.3.1", 39 | "twine>=4.0.2", 40 | ] 41 | 42 | [project.optional-dependencies] 43 | format = ["black >= 23.3.0"] 44 | 45 | [project.urls] 46 | repository = "https://github.com/manumerous/vpselector" 47 | -------------------------------------------------------------------------------- /src/vpselector/widgets/histogram_plot_widget.py: -------------------------------------------------------------------------------- 1 | __author__ = "Manuel Yves Galliker" 2 | __maintainer__ = "Manuel Yves Galliker" 3 | __license__ = "Apache-2.0" 4 | 5 | from vpselector.widgets.dataframe_plot_widget import DataFramePlotWidget 6 | 7 | import seaborn as sns 8 | import pandas as pd 9 | from overrides import override 10 | 11 | 12 | class HistogramPlotWidget(DataFramePlotWidget): 13 | def __init__(self, plot_config: dict, parentWindow): 14 | super(HistogramPlotWidget, self).__init__(plot_config, parentWindow) 15 | 16 | @override 17 | def plot(self, df: pd.DataFrame): 18 | for i in range(self.subplot_count): 19 | self.canvas.subplot_axes[i].clear() 20 | subplot_key = self.subplot_keys[i] 21 | subplot_topics_list = self.plot_config[subplot_key] 22 | sns.histplot( 23 | df[subplot_topics_list], 24 | ax=self.canvas.subplot_axes[i], 25 | stat="probability", 26 | ) 27 | 28 | self.canvas.draw() 29 | return 30 | 31 | def clear(self): 32 | for i in range(self.subplot_count): 33 | self.canvas.subplot_axes[i].clear() 34 | self.canvas.draw() 35 | return 36 | -------------------------------------------------------------------------------- /src/vpselector/widgets/time_series_data_plot_widget.py: -------------------------------------------------------------------------------- 1 | __author__ = "Manuel Yves Galliker" 2 | __maintainer__ = "Manuel Yves Galliker" 3 | __license__ = "Apache-2.0" 4 | 5 | from vpselector.widgets.dataframe_plot_widget import DataFramePlotWidget 6 | 7 | from matplotlib.widgets import SpanSelector 8 | import pandas as pd 9 | from overrides import override 10 | 11 | 12 | class TimeSeriesDataPlotWidget(DataFramePlotWidget): 13 | def __init__(self, plot_config: dict, parentWindow): 14 | super(TimeSeriesDataPlotWidget, self).__init__(plot_config, parentWindow) 15 | self.setup_span(parentWindow) 16 | 17 | def setup_span(self, parentWindow): 18 | for i in range(self.subplot_count): 19 | self.canvas.subplot_axes[i].span = SpanSelector( 20 | self.canvas.subplot_axes[i], 21 | onselect=parentWindow.on_region_select_callback, 22 | direction="horizontal", 23 | minspan=1, 24 | useblit=True, 25 | button=[1], 26 | props={"facecolor": "green", "alpha": 0.3}, 27 | ) 28 | 29 | @override 30 | def plot(self, df: pd.DataFrame): 31 | x_axis_data = df[self.x_axis_col] 32 | x_start = x_axis_data.iloc[0] 33 | x_end = x_axis_data.iloc[-1] 34 | self.canvas.subplot_axes[0].set_xlim([x_start, x_end]) 35 | 36 | for i in range(self.subplot_count): 37 | subplot_key = self.subplot_keys[i] 38 | subplot_topics_list = self.plot_config[subplot_key] 39 | for topic in subplot_topics_list: 40 | self.canvas.subplot_axes[i].plot( 41 | df[self.x_axis_col], df[topic], label=topic 42 | ) 43 | self.canvas.subplot_axes[i].legend(loc="lower right") 44 | 45 | self.canvas.draw() 46 | return 47 | 48 | def update_selection_visualitation(self, selection: dict): 49 | for subplot in self.canvas.subplot_axes: 50 | subplot.axvspan( 51 | selection["start"], selection["end"], facecolor="grey", alpha=0.3 52 | ) 53 | self.canvas.draw() 54 | return 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [![PyPI - Version](https://badge.fury.io/py/vpselector.svg)](https://pypi.org/project/vpselector/) 3 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/vpselector)](https://pypi.org/project/vpselector/) 4 | [![CI Pipeline](https://github.com/manumerous/vpselector/actions/workflows/check_style.yaml/badge.svg)](https://github.com/manumerous/vpselector/actions/workflows/check_style.yaml) 5 | 6 | # vpselector 7 | 8 | The Visual Pandas Selector is a tool to visually select portions of numeric time-series data from a pandas dataframe. The tool is intended to provide an fast interactive way for manual data selection, as can be very useful in for example machine learning, regression or system identification. 9 | 10 | Easily configure the tool to plot dataframe columns in vertically stacked subplots and view data distributions with the included histogram feature. With a simple click and drag, you can then select horizontal data windows, and let the tool automatically combine them into a new dataframe. 11 | 12 | The user can subsequentially select different horizontal data windows via click and drag and he tool then automatically combines the visually selected sections into a new dataframe. 13 | 14 | ![ezgif com-gif-maker(1)](https://github.com/manumerous/visual-pandas-curator/assets/18735094/b5ebbb99-d2f7-4901-b101-cbeed6c230aa) 15 | 16 | 17 | ## Install 18 | 19 | You can directly install the package in your terminal from [pypi.org](https://pypi.org/project/vpselector/) using: 20 | 21 | ```bash 22 | pip install vpselector 23 | ``` 24 | 25 | ## Use in your project 26 | 27 | Then simply import it using `import vpselector`. Then simply use: 28 | 29 | - If your project does not contain a pyqt application: `vpselector.select_visual_data(data : pd.DataFrame, plot_config : dict)` 30 | 31 | - To add the vpselector to an existing pyqt application: `vpselector.select_visual_data_in_pyqt_app(data : pd.DataFrame, plot_config : dict, pyqt_app)` 32 | 33 | 34 | ## Run the Example 35 | 36 | ```bash 37 | python3 vpselector_example.py 38 | ``` 39 | 40 | #### Use the Tool 41 | 42 | - Left click with your mouse and drag to define the desired horizontal window of the data to be selected. 43 | - The current selection distribution is now visualized in the histogram plot on the right. 44 | - Confirm or cancel data selection. 45 | - The already selected data is now marked by a grey span in the plot on the left. 46 | - The plot on the right contains now the histogram of all selected data. 47 | - repeat as many times as needed. 48 | - Once you could select all desired horizontal data windows click "Done selecting" 49 | -------------------------------------------------------------------------------- /src/vpselector/windows/main_window.py: -------------------------------------------------------------------------------- 1 | __author__ = "Manuel Yves Galliker" 2 | __maintainer__ = "Manuel Yves Galliker" 3 | __license__ = "Apache-2.0" 4 | 5 | 6 | from vpselector.windows.confirm_selection_window import ConfirmSelectionWindow 7 | from vpselector.widgets.time_series_data_plot_widget import TimeSeriesDataPlotWidget 8 | from vpselector.widgets.histogram_plot_widget import HistogramPlotWidget 9 | 10 | from PyQt5 import QtWidgets, QtCore 11 | from PyQt5.QtWidgets import QLabel, QPushButton 12 | from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT as NavigationToolbar 13 | 14 | import pandas as pd 15 | import time 16 | import copy 17 | 18 | pd.set_option("mode.chained_assignment", None) 19 | 20 | 21 | class MainWindow(QtWidgets.QMainWindow): 22 | def __init__(self, data : pd.DataFrame , plot_config : dict, *args, **kwargs): 23 | super(MainWindow, self).__init__(*args, **kwargs) 24 | self.setWindowTitle("Visual Pandas Selector") 25 | 26 | self.data = data 27 | self.cropped_data = pd.DataFrame() 28 | # list of selected tuples (start_index, end_index) 29 | self.selection_list = [] 30 | 31 | self.x_axis_data = self.data[plot_config["x_axis_col"]] 32 | 33 | self.data_plt = TimeSeriesDataPlotWidget(plot_config, parentWindow=self) 34 | self.data_plt.plot(self.data) 35 | self.hist_plt = HistogramPlotWidget(plot_config, parentWindow=self) 36 | 37 | # Create toolbar for simultaneous manipulation of all subplots 38 | self.addToolBar(NavigationToolbar(self.data_plt.canvas, self)) 39 | 40 | master_layout = QtWidgets.QGridLayout() 41 | 42 | self.termination_button = QPushButton("Done selecting") 43 | self.termination_button.setFixedSize(150, 30) 44 | # connect signal 45 | self.termination_button.clicked.connect(self._terminate) 46 | 47 | self.save_csv_button = QPushButton("Save Result") 48 | self.save_csv_button.setFixedSize(150, 30) 49 | self.save_csv_button.setEnabled(False) 50 | # connect signal 51 | self.save_csv_button.clicked.connect(self._save_to_csv) 52 | 53 | master_layout.setColumnMinimumWidth(0, 900) 54 | master_layout.setRowMinimumHeight(2, 500) 55 | 56 | self.time_series_plot_label = QLabel( 57 | "Click and drag to select data using the mouse." 58 | ) 59 | self.time_series_plot_label.setFixedHeight(50) 60 | self.hist_plot_label = QLabel("Histogram of all selected data.") 61 | master_layout.addWidget(self.time_series_plot_label, 1, 0) 62 | master_layout.addWidget(self.hist_plot_label, 1, 1) 63 | master_layout.addWidget(self.data_plt, 2, 0) 64 | master_layout.addWidget(self.hist_plt, 2, 1) 65 | master_layout.setColumnStretch(1, 0) 66 | 67 | button_grid_layout = QtWidgets.QGridLayout() 68 | button_grid_layout.addWidget( 69 | self.termination_button, 0, 1, alignment=QtCore.Qt.AlignRight 70 | ) 71 | button_grid_layout.addWidget( 72 | self.save_csv_button, 0, 0, alignment=QtCore.Qt.AlignRight 73 | ) 74 | button_grid = QtWidgets.QWidget() 75 | button_grid.setLayout(button_grid_layout) 76 | master_layout.addWidget(button_grid, 3, 1) 77 | master_layout.setRowStretch(2, 1) 78 | 79 | widget = QtWidgets.QWidget() 80 | widget.setLayout(master_layout) 81 | self.setCentralWidget(widget) 82 | 83 | self.show() 84 | 85 | def on_region_select_callback(self, min_x_val : int, max_x_val : int): 86 | cropped_df = self.crop_df(min_x_val, max_x_val) 87 | self.hist_plot_label.setText("Histogram of currently selected data.") 88 | self.hist_plt.plot(cropped_df) 89 | dialog_window = ConfirmSelectionWindow() 90 | selection_accepted = dialog_window.exec_() 91 | 92 | if selection_accepted: 93 | print("selection accepted and added: ", min_x_val, max_x_val) 94 | self.selection_list.append({"start": min_x_val, "end": max_x_val}) 95 | cropped_df["old_index"] = copy.deepcopy(cropped_df.index) 96 | self.cropped_data = pd.concat([self.cropped_data, cropped_df]) 97 | self.cropped_data = self.cropped_data.reset_index(drop=True) 98 | self.save_csv_button.setEnabled(True) 99 | self.data_plt.update_selection_visualitation(self.selection_list[-1]) 100 | if self.cropped_data.empty: 101 | self.hist_plt.clear() 102 | else: 103 | self.hist_plt.plot(self.cropped_data) 104 | self.hist_plot_label.setText("Histogram of all selected data.") 105 | 106 | return 107 | 108 | def crop_df(self, x_start : int, x_end : int): 109 | cropped_df = self.data[ 110 | (self.x_axis_data >= x_start) & (self.x_axis_data <= x_end) 111 | ] 112 | return cropped_df 113 | 114 | def _save_to_csv(self): 115 | file_dialog = QtWidgets.QFileDialog(self) 116 | timestr = time.strftime("%Y-%m-%d-%H-%M-%S") 117 | default_file_name = "output-" + timestr + ".csv" 118 | name = file_dialog.getSaveFileName( 119 | self, "Save Dataframe to csv", default_file_name 120 | ) 121 | file_path = name[0] 122 | if file_path[-4:] != ".csv": 123 | print("Wrong file name extension detected, adapting to *.csv") 124 | split_path = file_path.split(".", 1) 125 | file_path = split_path[0] + ".csv" 126 | print("Saving file to: ", file_path) 127 | self.cropped_data.to_csv(file_path, index=True) 128 | 129 | return 130 | 131 | def _terminate(self): 132 | self.close() 133 | return 134 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [2021] [Manuel Yves Galliker] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------