Commit 218b415b authored by Samuel GAIST's avatar Samuel GAIST

Merge branch '184_toolchain_editor' into 'master'

Toolchain Editor

Closes #184

See merge request !117
parents 0d6ff0f9 5a48ef4b
Pipeline #35622 passed with stages
in 6 minutes and 51 seconds
This diff is collapsed.
This diff is collapsed.
...@@ -50,13 +50,11 @@ from ..utils import check_prefix_dataformats ...@@ -50,13 +50,11 @@ from ..utils import check_prefix_dataformats
from ..widgets.mainwindow import MainWindow from ..widgets.mainwindow import MainWindow
from ..widgets.assetwidget import AssetWidget from ..widgets.assetwidget import AssetWidget
from ..widgets.toolchaineditor import SimpleToolchainPreview
from ..backend.asset import AssetType from ..backend.asset import AssetType
from ..backend.asset import Asset from ..backend.asset import Asset
from ..backend.eventfilters import MouseWheelFilter from ..backend.eventfilters import MouseWheelFilter
from .. import resources # noqa Qt resources system, only import is needed
from .. import version from .. import version
global logger global logger
...@@ -203,36 +201,3 @@ def refresh_env(ctx, type_): ...@@ -203,36 +201,3 @@ def refresh_env(ctx, type_):
if os.path.exists(environments_file_path): if os.path.exists(environments_file_path):
os.remove(environments_file_path) os.remove(environments_file_path)
ctx.invoke(environments.list, type_=type_, output=environments_file_path) ctx.invoke(environments.list, type_=type_, output=environments_file_path)
@editor.group(cls=AliasedGroup)
@click.pass_context
def toolchain(ctx):
"""Toolchain specific commands"""
pass
SHOW_TOOLCHAIN_EPILOG = """\b
Example:
$ beat editor toolchain show user/my_toolchain/1
"""
@toolchain.command(epilog=EDIT_EPILOG)
@click.argument("asset_name")
@click.pass_context
@refresh_environment_cache_flag
@raise_on_error
def show(ctx, asset_name):
"""Edit one specific asset"""
app = QApplication(sys.argv)
asset = Asset(ctx.meta["config"].path, AssetType.TOOLCHAIN, asset_name)
preview = SimpleToolchainPreview()
preview.load(asset.declaration)
preview.show()
return app.exec_()
...@@ -59,9 +59,9 @@ from ..widgets.experimenteditor import EnvironmentModel ...@@ -59,9 +59,9 @@ from ..widgets.experimenteditor import EnvironmentModel
from ..widgets.experimenteditor import ExperimentEditor from ..widgets.experimenteditor import ExperimentEditor
from ..widgets.experimenteditor import typed_user_property from ..widgets.experimenteditor import typed_user_property
from ..widgets.experimenteditor import ExperimentResources from ..backend.resourcemodels import ExperimentResources
from ..widgets.experimenteditor import AlgorithmResourceModel from ..backend.resourcemodels import AlgorithmResourceModel
from ..widgets.experimenteditor import QueueResourceModel from ..backend.resourcemodels import QueueResourceModel
from .conftest import prefix from .conftest import prefix
from .conftest import sync_prefix from .conftest import sync_prefix
......
# vim: set fileencoding=utf-8 :
###############################################################################
# #
# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/ #
# Contact: beat.support@idiap.ch #
# #
# This file is part of the beat.editor module of the BEAT platform. #
# #
# Commercial License Usage #
# Licensees holding valid commercial BEAT licenses may use this file in #
# accordance with the terms contained in a written agreement between you #
# and Idiap. For further information contact tto@idiap.ch #
# #
# Alternatively, this file may be used under the terms of the GNU Affero #
# Public License version 3 as published by the Free Software and appearing #
# in the file LICENSE.AGPL included in the packaging of this file. #
# The BEAT platform is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
# or FITNESS FOR A PARTICULAR PURPOSE. #
# #
# You should have received a copy of the GNU Affero Public License along #
# with the BEAT platform. If not, see http://www.gnu.org/licenses/. #
# #
###############################################################################
import pytest
from PyQt5.QtSql import QSqlQuery
from PyQt5.QtSql import QSqlTableModel
from beat.backend.python.algorithm import Algorithm
from ..backend.resourcemodels import ExperimentResources
from ..backend.resourcemodels import AlgorithmResourceModel
from ..backend.resourcemodels import DatasetResourceModel
# ------------------------------------------------------------------------------
# Tests
class TestExperimentResources:
"""Test that the prefix modelisation generates the expected data"""
def test_model(self, beat_context):
model = ExperimentResources(beat_context)
model = QSqlTableModel()
model.setTable("algorithms")
model.select()
total = model.rowCount()
assert total > 0
model.setFilter("is_analyzer=True")
analyzer_count = model.rowCount()
assert analyzer_count > 0
assert analyzer_count < total
class TestAlgorithmResourceModel:
"""Test the model used to generate suitable algorithm selections"""
@pytest.fixture
def prefix_model(self, beat_context):
return ExperimentResources(beat_context)
def test_default(self, prefix_model):
algorithm_model = AlgorithmResourceModel()
query = QSqlQuery()
assert query.exec_(
"SELECT COUNT(name) AS cnt FROM algorithms WHERE is_analyzer=false"
)
query.next()
assert algorithm_model.rowCount() == query.value("cnt")
def test_analyzers(self, prefix_model):
algorithm_model = AlgorithmResourceModel()
algorithm_model.setAnalyzerEnabled(True)
query = QSqlQuery()
assert query.exec_(
"SELECT COUNT(name) AS cnt FROM algorithms WHERE is_analyzer=true"
)
query.next()
assert algorithm_model.rowCount() == query.value("cnt")
def test_types(self, prefix_model):
algorithm_model = AlgorithmResourceModel()
algorithm_model.setTypes(
[Algorithm.LEGACY, Algorithm.SEQUENTIAL, Algorithm.AUTONOMOUS]
)
query = QSqlQuery()
assert query.exec_(
"SELECT COUNT(name) AS cnt FROM algorithms WHERE is_analyzer=false AND type IN ('legacy', 'sequential', 'autonomous')"
)
query.next()
assert algorithm_model.rowCount() == query.value("cnt")
def test_output_count(self, prefix_model):
algorithm_model = AlgorithmResourceModel()
algorithm_model.setOutputCount(2)
query = QSqlQuery()
assert query.exec_(
"SELECT COUNT(name) AS cnt FROM algorithms WHERE is_analyzer=false AND outputs='2'"
)
query.next()
assert algorithm_model.rowCount() == query.value("cnt")
def test_input_count(self, prefix_model):
algorithm_model = AlgorithmResourceModel()
algorithm_model.setInputCount(2)
query = QSqlQuery()
assert query.exec_(
"SELECT COUNT(name) AS cnt FROM algorithms WHERE is_analyzer=false AND inputs='2'"
)
query.next()
assert algorithm_model.rowCount() == query.value("cnt")
def test_input_output_count(self, prefix_model):
algorithm_model = AlgorithmResourceModel()
algorithm_model.setInputCount(1)
algorithm_model.setOutputCount(1)
query = QSqlQuery()
assert query.exec_(
"SELECT COUNT(name) AS cnt FROM algorithms WHERE is_analyzer=false AND inputs='1' AND outputs='1'"
)
query.next()
assert algorithm_model.rowCount() == query.value("cnt")
def test_input_output_count_and_types(self, prefix_model):
algorithm_model = AlgorithmResourceModel()
algorithm_model.setInputCount(1)
algorithm_model.setOutputCount(1)
algorithm_model.setTypes([Algorithm.SEQUENTIAL])
query = QSqlQuery()
assert query.exec_(
"SELECT COUNT(name) AS cnt FROM algorithms WHERE is_analyzer=false AND inputs='1' AND outputs='1' AND type IN ('sequential')"
)
query.next()
assert algorithm_model.rowCount() == query.value("cnt")
class TestDatasetResourceModel:
"""Test the model used to generate suitable dataset selections"""
@pytest.fixture
def prefix_model(self, beat_context):
return ExperimentResources(beat_context)
def test_default(self, prefix_model):
dataset_model = DatasetResourceModel()
query = QSqlQuery()
assert query.exec_("SELECT COUNT(name) AS cnt FROM datasets")
query.next()
assert dataset_model.rowCount() == query.value("cnt")
def test_output_count(self, prefix_model):
dataset_model = DatasetResourceModel()
dataset_model.setOutputCount(2)
query = QSqlQuery()
assert query.exec_("SELECT COUNT(name) AS cnt FROM datasets WHERE outputs='2'")
query.next()
assert dataset_model.rowCount() == query.value("cnt")
...@@ -58,6 +58,7 @@ class TestToolchainEditor: ...@@ -58,6 +58,7 @@ class TestToolchainEditor:
@pytest.mark.parametrize("toolchain", get_valid_toolchains(prefix)) @pytest.mark.parametrize("toolchain", get_valid_toolchains(prefix))
def test_load_and_dump(self, qtbot, test_prefix, toolchain): def test_load_and_dump(self, qtbot, test_prefix, toolchain):
reference_json = get_toolchain_declaration(test_prefix, toolchain) reference_json = get_toolchain_declaration(test_prefix, toolchain)
editor = ToolchainEditor() editor = ToolchainEditor()
......
...@@ -22,3 +22,5 @@ ...@@ -22,3 +22,5 @@
# with the BEAT platform. If not, see http://www.gnu.org/licenses/. # # with the BEAT platform. If not, see http://www.gnu.org/licenses/. #
# # # #
############################################################################### ###############################################################################
from .. import resources # noqa Qt resources system, only import is needed
# vim: set fileencoding=utf-8 :
###############################################################################
# #
# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/ #
# Contact: beat.support@idiap.ch #
# #
# This file is part of the beat.editor module of the BEAT platform. #
# #
# Commercial License Usage #
# Licensees holding valid commercial BEAT licenses may use this file in #
# accordance with the terms contained in a written agreement between you #
# and Idiap. For further information contact tto@idiap.ch #
# #
# Alternatively, this file may be used under the terms of the GNU Affero #
# Public License version 3 as published by the Free Software and appearing #
# in the file LICENSE.AGPL included in the packaging of this file. #
# The BEAT platform is distributed in the hope that it will be useful, but #
# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY #
# or FITNESS FOR A PARTICULAR PURPOSE. #
# #
# You should have received a copy of the GNU Affero Public License along #
# with the BEAT platform. If not, see http://www.gnu.org/licenses/. #
# #
###############################################################################
from PyQt5.QtCore import QLineF
from PyQt5.QtWidgets import QGraphicsScene
from PyQt5.QtGui import QColor
from PyQt5.QtGui import QPen
class DrawingSpace(QGraphicsScene):
"""Playground scene for block objects"""
def __init__(self, configuration):
super().__init__()
self.grid_size = configuration["grid_size"]
self.grid_color = configuration["grid_color"]
def drawBackground(self, painter, rect):
"""Background grid"""
leftVerticalLine = rect.left() - rect.left() % self.grid_size
topHorizontalLine = rect.top() - rect.top() % self.grid_size
lines = []
for i in range(int(leftVerticalLine), int(rect.right()), self.grid_size):
lines.append(QLineF(i, rect.top(), i, rect.bottom()))
for i in range(int(topHorizontalLine), int(rect.bottom()), self.grid_size):
lines.append(QLineF(rect.left(), i, rect.right(), i))
self.pen = QPen()
self.pen.setColor(QColor(*self.grid_color))
self.pen.setWidth(0)
painter.setPen(self.pen)
painter.drawLines(lines)
...@@ -52,10 +52,6 @@ from PyQt5.QtWidgets import QTabWidget ...@@ -52,10 +52,6 @@ from PyQt5.QtWidgets import QTabWidget
from PyQt5.QtWidgets import QVBoxLayout from PyQt5.QtWidgets import QVBoxLayout
from PyQt5.QtWidgets import QWidget from PyQt5.QtWidgets import QWidget
from PyQt5.QtSql import QSqlDatabase
from PyQt5.QtSql import QSqlQuery
from PyQt5.QtSql import QSqlTableModel
from beat.core.experiment import PROCESSOR_PREFIX from beat.core.experiment import PROCESSOR_PREFIX
from beat.core.experiment import EVALUATOR_PREFIX from beat.core.experiment import EVALUATOR_PREFIX
...@@ -64,6 +60,9 @@ from beat.backend.python.algorithm import Algorithm ...@@ -64,6 +60,9 @@ from beat.backend.python.algorithm import Algorithm
from ..backend.asset import Asset from ..backend.asset import Asset
from ..backend.asset import AssetType from ..backend.asset import AssetType
from ..backend.assetmodel import AssetModel from ..backend.assetmodel import AssetModel
from ..backend.resourcemodels import ExperimentResources
from ..backend.resourcemodels import AlgorithmResourceModel
from ..backend.resourcemodels import QueueResourceModel
from ..decorators import frozen from ..decorators import frozen
...@@ -240,230 +239,8 @@ class DatasetModel(QStringListModel): ...@@ -240,230 +239,8 @@ class DatasetModel(QStringListModel):
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Prefix modelization
class ExperimentResources:
"""Modelization of the experiments resources"""
def __init__(self, context=None):
self.context = context
database = QSqlDatabase.addDatabase("QSQLITE")
database.setDatabaseName(":memory:")
if not database.open():
raise RuntimeError(
f"Failed to open database: {database.lastError().text()}"
)
self.refresh()
def setContext(self, context):
if self.context == context:
return
self.context = context
self.refresh()
def refresh(self):
ALGORITHM_TABLE_CLEANUP = "DROP TABLE IF EXISTS algorithms"
ALGORITHM_TABLE = "CREATE TABLE algorithms(name varchar, type varchar, inputs integer, outputs integer, is_analyzer boolean)"
INSERT_ALGORITHM = "INSERT INTO algorithms(name, type, inputs, outputs, is_analyzer) VALUES(?, ?, ?, ?, ?)"
QUEUE_TABLE_CLEANUP = "DROP TABLE IF EXISTS queues"
QUEUE_TABLE = "CREATE TABLE queues(name varchar, env_name varchar, env_version varchar, env_type varchar)"
INSERT_QUEUE = "INSERT INTO queues(name, env_name, env_version, env_type) VALUES (?, ?, ?, ?)"
query = QSqlQuery()
for query_str in [ALGORITHM_TABLE_CLEANUP, QUEUE_TABLE_CLEANUP]:
if not query.exec_(query_str):
raise RuntimeError(f"Failed to drop table: {query.lastError().text()}")
for query_str in [ALGORITHM_TABLE, QUEUE_TABLE]:
if not query.exec_(query_str):
raise RuntimeError(
f"Failed to create table: {query.lastError().text()}"
)
if self.context is None:
return
prefix_path = self.context.meta["config"].path
model = AssetModel()
model.asset_type = AssetType.ALGORITHM
model.prefix_path = prefix_path
model.setLatestOnlyEnabled(False)
if not query.prepare(INSERT_ALGORITHM):
raise RuntimeError(f"Failed to prepare query: {query.lastError().text()}")
for algorithm in model.stringList():
asset = Asset(prefix_path, AssetType.ALGORITHM, algorithm)
try:
declaration = asset.declaration
except json.JSONDecodeError:
continue
inputs = {}
outputs = {}
for group in declaration["groups"]:
inputs.update(group.get("inputs", {}))
outputs.update(group.get("outputs", {}))
query.addBindValue(algorithm)
query.addBindValue(declaration.get("type", "legacy"))
query.addBindValue(len(inputs))
query.addBindValue(len(outputs))
query.addBindValue("results" in declaration)
if not query.exec_():
raise RuntimeError(
f"Failed to insert algorithm: {query.lastError().text()}"
)
if not query.prepare(INSERT_QUEUE):
raise RuntimeError(f"Failed to prepare query: {query.lastError().text()}")
environments_path = self.context.meta["environments"]
with open(environments_path, "rt") as file:
environment_data = json.load(file)
for item in environment_data.get("remote", []):
env_name = item["name"]
env_version = item["version"]
# import ipdb; ipdb.set_trace()
for name in item["queues"].keys():
query.addBindValue(name)
query.addBindValue(env_name)
query.addBindValue(env_version)
query.addBindValue("remote")
if not query.exec_():
raise RuntimeError(
f"Failed to insert queue: {query.lastError().text()}"
)
for _, image_info in environment_data.get("docker", {}).items():
env_name = image_info["name"]
env_version = image_info["version"]
query.addBindValue("Local")
query.addBindValue(env_name)
query.addBindValue(env_version)
query.addBindValue("docker")
if not query.exec_():
raise RuntimeError(
f"Failed to insert queue: {query.lastError().text()}"
)
class AlgorithmResourceModel(QSqlTableModel):
def __init__(self, parent=None):
super().__init__(parent=parent)
self._analyzer_enabled = False
self._input_count = None
self._output_count = None
self._types = []
self.setTable("algorithms")
self.select()
self.update()
def update(self):
filter_str = f"is_analyzer={self._analyzer_enabled}"
if self._input_count is not None:
filter_str += f" AND inputs={self._input_count}"
if self._output_count is not None:
filter_str += f" AND outputs={self._output_count}"
if self._types:
filter_str += " AND type in ({})".format(
",".join([f"'{type_}'" for type_ in self._types])
)
self.setFilter(filter_str)
def setAnalyzerEnabled(self, enabled):
if self._analyzer_enabled == enabled:
return
self._analyzer_enabled = enabled
self.update()
def setInputCount(self, count):
if self._input_count == count:
return
self._input_count = count
self.update()
def setOutputCount(self, count):
if self._output_count == count:
return
self._output_count = count
self.update()
def setTypes(self, type_list):
if self._types == type_list:
return
self._types = type_list
self.update()
class QueueResourceModel(QSqlTableModel):
def __init__(self, parent=None):
super().__init__(parent=parent)
self._environment = None
self._version = None
self._type = None
self.setTable("queues")
self.select()
self.update()
def update(self):
filter_str = ""
if self._environment is not None:
filter_str += f"env_name='{self._environment}'"
if self._version is not None:
if filter_str:
filter_str += " AND "
filter_str += f"env_version='{self._version}'"
if self._type is not None:
if filter_str:
filter_str += " AND "
filter_str += f"env_type='{self._type}'"
self.setFilter(filter_str)
def setEnvironment(self, name, version):
if self._environment == name and self._version == version:
return
self._environment = name
self._version = version
self.update()
def setType(self, type_):
if self._type == type_:
return
self._type = type_
self.update()
def dump(self):
print(self.filter())
for i in range(self.rowCount()):
print([self.index(i, j).data() for j in range(4)])
# ------------------------------------------------------------------------------
# Editors # Editors
......
This diff is collapsed.
<!DOCTYPE RCC><RCC version="1.0"> <!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="resources"> <qresource prefix="resources">
<file alias="remap">resources/remap.png</file> <file alias="remap">resources/img/remap.png</file>
<file alias="docker">resources/icons8-docker-50.png</file> <file alias="docker">resources/img/icons8-docker-50.png</file>
<file alias="remote">resources/icons8-cloud-development-50.png</file> <file alias="remote">resources/img/icons8-cloud-development-50.png</file>
<file alias="dataset">resources/img/dataset.png</file>
<file alias="block">resources/img/block.png</file>
<file alias="loop">resources/img/loop.png</file>
<file alias="analyzer">resources/img/analyzer.png</file>
<file alias="toolchain_style_config">resources/json/toolchain_style_config.json</file>
</qresource> </qresource>
</RCC> </RCC>
<mxfile host="www.draw.io" modified="2019-11-08T15:52:54.936Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36" etag="1Ma9UdnNdw8-at8-L0FF" version="12.2.3" type="device" pages="1"><diagram id="rtFZ-Xy4-K91k_U5cjYo" name="Page-1">rZRBb4MgFIB/jcclUmqi19l2O2ynHpYdibwKGfoM4tT++mGFquu6rsk8GPje4wEfaEDTonvSrBKvyEEFq5B3Ad0Eq1VMqH0PoB/BmoYjyLXkIyIT2MsjOOjTGsmhXiQaRGVktYQZliVkZsGY1tgu0w6olrNWLIcLsM+YuqRvkhvhthWFE38GmQs/MwldpGA+2YFaMI7tDNFtQFONaMZW0aWgBnfeyzhudyV6XpiG0vxlwDpWYaOPvdBCZEwk7+nL5sFV+WSqcRt2izW9N6CxKTkMRUhAH1shDewrlg3R1h65ZcIUyoUPUqkUFerTWBrHScRjy2uj8QNmkfD02IhbAGgD3dWdkbMve88ACzC6tylugDfcL7vtdF5rh8TsqDxj7obk57qTRNtwHu9wSm87tVXsBYbbPlldjbf6ILvhDH4R+U39bpck/yWYRAvBNLoQTH4QTO4XbLvTB3GKzf4qdPsF</diagram></mxfile>
<mxfile host="www.draw.io" modified="2019-11-08T15:51:32.410Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36" etag="FMHzNpQ6OiY4Fj2sp-DL" version="12.2.3" type="device" pages="1"><diagram id="rtFZ-Xy4-K91k_U5cjYo" name="Page-1">3ZRBb4MgFIB/jcclIpq019l2O2ynHpYdibwKGYpBnNpfP1Sosq7rlmw7jIOR7z0e8EEIcFp0d4pU7FFSEEEU0i7AmyCKVgib7wD6CcQ4nECuOJ0QmsGeH8FCl9ZwCrWXqKUUmlc+zGRZQqY9RpSSrZ92kMKftSI5nIF9RsQ5feJUM7utJJz5PfCcuZlRaCMFcckW1IxQ2S4Q3gY4VVLq6a/oUhCDO+dlGre7ED0tTEGpvzIgXomwUceeKcYywtbP6cPmxlZ5JaKxG7aL1b0zoGRTUhiKoADftoxr2FckG6KtOXLDmC6EDR+4EKkUUo1jcTY2w2ut5AssIuHYTMQuAJSG7uLO0MmXuWcgC9CqNyl2gDPc+912Pq/YIrY4KseIvSH5qe4s0fxYj99wGl13aqqYCwzXfZK6mm71gXfDGXwi8p363W69/inBSeIJxsmZYPSBYPRbgvG/E4z+SrDpzi/OGFs823j7Bg==</diagram></mxfile>
<mxfile host="www.draw.io" modified="2019-11-08T15:49:42.011Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36" etag="yBG1uE0d3HswN5iHmqiN" version="12.2.3" type="device" pages="1"><diagram id="rtFZ-Xy4-K91k_U5cjYo" name="Page-1">rZQ9b4MwEEB/DWMljINE1pLQDu2UoepowQVbNRgZUyC/viYcAYumaaQwIPudzx/vDB6Ni+5Fs4q/qwykF/hZ59GdFwQRofY9gH4EmzAaQa5FNiIyg4M4AUIfaSMyqJ2BRilpROXCVJUlpMZhTGvVusOOSrqrViyHFTikTK7ph8gMx2OF/sxfQeR8Wpn4GCnYNBhBzVmm2gWie4/GWikztoouBjm4m7yMecmV6GVjGkrzn4RNJP1Gn3quOU8Z337Gb7snnOWbyQYPjJs1/WRAq6bMYJiEePS55cLAoWLpEG1tyS3jppAYPgopYyWVPufSJEn2cWR5bbT6gkXEPz82ghsAbaC7ejJy8WXvGagCjO7tEEyYDPdut53rtUHEF6WaGMMbkl/mnSXaBnq8w2lw26mdxV5guO2T1dV4q4+iG2rwh8iV+u32UYLD0BFMw5Vg8otgcr9g250/iHNs8Veh+x8=</diagram></mxfile>
<mxfile host="www.draw.io" modified="2019-11-25T09:02:19.235Z" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/73.0.3683.75 Safari/537.36" etag="BVvmCFOy7xHZZeS6J3PG" version="12.2.9" type="device" pages="1"><diagram id="rtFZ-Xy4-K91k_U5cjYo" name="Page-1">3ZZNb4IwGIB/DccllKLDq/ixg+5ismWnpaGvtlmxpNYB/voVKQLDTZdtuowDaZ+3n0/fFBwcxtlUkYTNJQXheC7NHDxyPM+77Zt3AfISIB+VYKU4tagGC74DC11Lt5zCptVQSyk0T9owkus1RLrFiFIybTdbStGeNSEr6IBFRESXPnKqWUmDnlvzO+ArVs2MXBuJSdXYgg0jVKYNhMcODpWUuizFWQiicFd5KftNPogeFqZgrc/p4AfC3apdzhRjEWGDp3A2urGjvBKxtRu2i9V5ZUDJ7ZpCMQhy8DBlXMMiIVERTc2RG8Z0LGx4yYUIpZBq3xcP+6MgCAzfaCVfoBFx94+JdLdRrQmUhqyB7LamIGPQKjdNqiSzhvN2Na3Py7eINY6qYsRmyOowbi3RFKzHLzj1Tjs1o5gEhtM+ySYps3rJs+IMPhH5Tv1kMhj8lOBery046AhGRwSj3xKM/51gdDXBz/6CDtW0F0JGp4jcP/Tns3NuhcsL7tg84vzcDPbxdQX/ySviW4LRpQSbav3N3McaPx54/AY=</diagram></mxfile>
{
"drawing_space_config":{
"scene_width": 2000,
"scene_height": 2000,
"grid_size": 36,
"grid_color": [232, 232, 232, 255]
},
"block_config":{
"width": 200,
"height": 25,
"radius": 10,
"border": 1,
"font": "Arial",
"font_size": 12,
"pin_height": 30,
"pin_font": "Arial",
"pin_font_size": 10,
"pin_color": [255, 155, 0, 255],
"background_color": [80, 80, 80, 255],
"background_color_datasets": [255, 254, 200, 255],
"background_color_blocks": [204, 204, 204, 255],
"background_color_analyzers": [136, 150, 216, 255],
"background_color_loops": [182, 216, 136, 255],
"border_color": [50, 50, 50, 255],
"selection_border_color": [170, 80, 80, 255],
"text_color": [0, 0, 0, 255]
},
"connection_config":{
"width": 2,
"color": [255, 180, 0, 255]
}
}