# Graphical interface for BooN design and analysis
# Author: Franck Delaplace
# Creation date: February 2024
# In comments :
# DEF means definition which is a code part gathering functions related to a process or an object definition,
# STEP means main steps
# WARNING is a warning.
# These terms can be used to color comments in PyCharm or else.
import sys
import os
import math
import re
from tabulate import tabulate
import BooNGui.booneries_rc # resources
import boon
from boon import BooN, SIGNCOLOR, COLORSIGN, EXTSBML, EXTXT, BOONSEP, PYTHONSKIP, PYTHONHEADER
import boon.logic as logic
from boon.logic import LOGICAL, SYMPY, MATHEMATICA, JAVA, BOOLNET
from sympy.logic.boolalg import is_cnf, is_dnf, is_nnf
from sympy.core.symbol import Symbol
from sympy.logic.boolalg import And, Not
from sympy import SOPform, symbols
from sympy.parsing.sympy_parser import parse_expr
from netgraph import EditableGraph
import networkx as nx
from PyQt5.QtWidgets import *
from PyQt5.uic import loadUi
from PyQt5.QtGui import QIcon, QStandardItemModel, QStandardItem
from PyQt5.QtCore import Qt, QSize
from PyQt5.QtCore import QObject, QThread, pyqtSignal
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWebEngineWidgets import QWebEngineView
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas
from matplotlib.figure import Figure
mpl.use("Qt5Agg")
# Parameters
HSIZE: int = 10 # size of the history
STYLE: dict = {"Logical": LOGICAL, "Java": JAVA, "Python": SYMPY, "Mathematica": MATHEMATICA, "BoolNet": BOOLNET}
ICON01: dict = {None: ":/icon/resources/none.svg", True: ":/icon/resources/true.svg", False: ":/icon/resources/false.svg"} # True/False icons
MODELBOUND: int = 8 # Size bound of the dynamics model in terms of variables.
# Node names for the pattern network
REG: str = '\u03b1' # lambda
POS: str = '\u03b2' # alpha
NEG: str = '\u03bb' # beta
# Integer regular expression
INTPAT: str = r"\s*-?[0-9]+\s*"
[docs]
class Boonify(QMainWindow):
"""Main Class of the GUI"""
def __init__(self):
super(Boonify, self).__init__()
loadUi('BooNGui/boonify.ui', self)
self.setGeometry(600, 100, 800, 800)
# STEP: initialize the Gui state
self.boon = BooN() # Current BooN
self.filename = "" # Current filename
self.history = [None] * HSIZE # History
self.hindex = 0 # Index of the last BooN added in the history.
self.hupdate = False # Flag determining whether the history is updated.
self.saved = True # Flag determining whether the current BooN is saved.
self.QView = None # Widget of the View
self.QStableStates = None # Widget of the stable states
self.QModel = None # Widget of the dynamics model
self.QControllability = None # Widget of the controllability
self.editgraph = None # Graph for the edition
self.disablecallback = True # Flag indicating whether the BooN design callback function is enabled, initially disabled (True) because the editgraph is not set up.
self.designsize = 2. # Size related to the EditableGraph and used for many parameters. It is modified when the window is rescaled to let the nodes and edges width invariant.
self.display_saved_flag() # Show the save flag in the status bar
# STEP: Connect callback functions to Menu
# File Management
self.ActionOpen.triggered.connect(self.open)
self.ActionSave.triggered.connect(self.save)
self.ActionSaveAs.triggered.connect(self.saveas)
self.ActionImport.triggered.connect(self.importation)
self.ActionExport.triggered.connect(self.exportation)
self.ActionQuit.triggered.connect(self.quit)
# Help
self.ActionHelp.triggered.connect(self.help)
self.ActionUndo.triggered.connect(self.undo)
self.ActionRedo.triggered.connect(self.redo)
# Network Management
self.ActionView.triggered.connect(self.view)
self.ActionModel.triggered.connect(self.model)
self.ActionStableStates.triggered.connect(self.stablestates)
self.ActionControllability.triggered.connect(self.controllability)
# STEP: Initialization of a thread for long run application --> controllability
# The thread is created and started.
self.worker = Threader()
# STEP: Initialize the Canvas for network design
fig = plt.figure() # plt.figure is necessary to create a manager
manager = fig.canvas.manager
self.canvas = FigureCanvas(fig)
self.canvas.axes = self.canvas.figure.add_subplot(111)
self.canvas.figure.subplots_adjust(left=0, bottom=0, right=1, top=1) # Adjust the window s.t. the network fully fills it.
self.canvas.figure.canvas.manager = manager # Assign the manager in the canvas to be accessible by EditGraph.
# STEP: Enable key_press_event events for using EditGraph:
self.canvas.setFocusPolicy(QtCore.Qt.ClickFocus)
self.canvas.setFocus()
# STEP: Insert the network design figure in the main window.
self.DesignCanvas.addWidget(self.canvas)
# STEP: set up network design
self.setup_design()
# DEF: NETWORK DESIGN
[docs]
def setup_design(self):
"""Set up of the editable graph."""
self.disablecallback = True # Disable the design callback during the set-up.
# STEP: Creation of the pattern-network used for edge styling.
# To prevent its inclusion in the BooN, the variable names are strings while the other nodes are integers or symbols.
# network structure: NEG <--[-]-- REG --[+]--> POS
g = nx.DiGraph([(REG, POS), (REG, NEG)])
edge_color = {(REG, POS): SIGNCOLOR[1], (REG, NEG): SIGNCOLOR[-1], (NEG, NEG): SIGNCOLOR[-1], (POS, POS): SIGNCOLOR[1]}
positions = {NEG: (0.25, 0.045), REG: (0.5, 0.045), POS: (0.75, 0.045)}
# STEP: Add the interaction graph of the current BooN.
ig = self.boon.interaction_graph # Get the IG.
g.add_nodes_from(ig.nodes())
g.add_edges_from(ig.edges())
# STEP: Find the edge color from signs.
signs = nx.get_edge_attributes(ig, 'sign')
edge_color.update({edge: SIGNCOLOR[signs[edge]] for edge in signs}) # Transform sign in color.
positions.update(self.boon.pos) # Complete the position.
# STEP: Convert the module ids to string labeling the edge.
modules = nx.get_edge_attributes(ig, 'module')
modules = {edge: " ".join([str(module) for module in modules[edge]]) for edge in modules}
# STEP: Define the editgraph from the information previously extracted.
self.canvas.axes.clear()
self.canvas.axes.set_xlim(-1000, 1000)
# WARNING : the definition of EditableGraph is the same as the definition in resizeEvent function. Any modification of one must be reported to the other.
self.editgraph = EditableGraph(g,
node_labels=True,
node_color='antiquewhite',
node_edge_color='black',
node_label_fontdict=dict(family='sans-serif', color='black', weight='semibold', fontsize=11),
node_layout=positions,
node_size=self.designsize,
node_edge_width=self.designsize / 4,
node_label_offset=(0., 0.025 * self.designsize),
edge_width=self.designsize / 2,
arrows=True,
edge_labels=modules,
edge_label_position=0.75,
edge_label_fontdict=dict(family='sans-serif', fontweight='bold', fontsize=8, color='royalblue'),
edge_color=edge_color,
ax=self.canvas.axes)
self.disablecallback = False # Enable the design callback.
[docs]
def design(self):
"""interaction graph design callback."""
# The function captures the events of the EditableGraph and completes the BooN.
if self.disablecallback: return # Check whether the callback is enabled otherwise return.
# DEF: Definition of the interaction graph of the network from the drawing.
# The graph was derived from the graph drawing process.
# Arc signs are associated with their color.
# Only nodes with a label are used to define the graph.
# STEP: Find consistent nodes corresponding to the variables in the BooN.
# Consistent nodes are symbols or integers.
# Dictionary of consistent nodes id:symbol, where the symbol is defined from the node label.
# Recall that only the labeled nodes are kept in editgraph.node_label_artists. The unlabelled nodes are not considered.
# WARNING: As nodes of the pattern network are strings, they are never selected as consistent nodes.
idvar = {idt: symbols(text.get_text()) for idt, text in self.editgraph.node_label_artists.items() if isinstance(idt, int | Symbol)}
# Function setting edges in symbolic form where nodes are all symbols.
def symbolic(edge):
"""set edges in symbolic form where node labels are symbols"""
return idvar[edge[0]], idvar[edge[1]]
# STEP: Select the edges with consistent nodes. The other edges cannot be included in the IG.
edges = {(src, tgt) for src, tgt in self.editgraph.edge_artists.keys() if src in idvar and tgt in idvar}
# STEP: define the positions of nodes. The positions are kept in a dictionary { node:pos ..}
edit_pos = self.editgraph.node_positions
pos = {idvar[idt]: edit_pos[idt] for idt in idvar}
# STEP: Find signs from the edge colors.
# WARNING: The face color is (r,g,b,a) while the sign colors are (r,g,b). Hence a must be removed.
edge_attributes = self.editgraph.edge_artists
signs = {edge: COLORSIGN[edge_attributes[edge].get_facecolor()[0:3]] for edge in edges}
# STEP: Determination of the modules from edge label.
edge_labels = self.editgraph.edge_label_artists
modules = {}
for edge in edges:
try:
label = edge_labels[edge].get_text()
# A module is a list of signed integers separated by spaces. The labels that are not integers are not considered.
modularity = {int(module) for module in list(label.split(" ")) if re.match(INTPAT, module)}
if modularity:
# re-set the label sign w.r.t. the edge sign, used if the user does not properly define the labels.
match signs[edge]:
case 1: # Positive
modularity = {abs(module) for module in modularity}
case -1: # Negative
modularity = {-abs(module) for module in modularity}
case 0: # undefined (both) - non-monotone interaction
pass
modules.update({symbolic(edge): modularity})
else:
modules.update({symbolic(edge): {signs[edge]}})
except KeyError: # No edge labels = no modules ⇒ the module is the sign.
modules.update({symbolic(edge): {signs[edge]}})
# STEP: Convert the edges symbolically for signs.
# WARNING: as signs dict is previously used for determining the modules with string as keys, the conversion can only be achieved here.
signs = {symbolic(edge): signs[edge] for edge in signs}
# DEF conversion of the IG to BooN
# STEP: Define the ig from the editable graph.
ig = nx.DiGraph()
ig.add_nodes_from(idvar.values()) # add nodes
for edge in signs: # add edges
ig.add_edge(edge[0], edge[1], sign=signs[edge], module=modules[edge])
# STEP: Convert the ig to BooN.
try:
self.boon = BooN.from_ig(ig) # Find the BooN from the ig.
except ValueError: # To prevent the transient error due to edge labeling.
pass
# STEP: Store positions.
self.boon.pos = pos
# DEF: Update the history and refresh the open windows.
self.add_history()
if self.hupdate:
self.refresh()
[docs]
def resizeEvent(self, event, **kwargs):
"""Modify graph parameters so that node and edge widths are invariant to window resizing."""
# STEP: Fix the size related to the network design
width = self.frameGeometry().width()
height = self.frameGeometry().height()
self.designsize = min(round(1350 / min(width, height), 1), 2.5) # heuristic function providing the designsize w.r.t. to window size.
# STEP: retrieve all parameters of editgraph
positions = self.editgraph.node_positions
edge_color = {edge: self.editgraph.edge_artists[edge].get_facecolor() for edge in self.editgraph.edge_artists}
modules = {edge: self.editgraph.edge_label_artists[edge].get_text() for edge in self.editgraph.edge_label_artists}
node_labels = {idt: text.get_text() for idt, text in self.editgraph.node_label_artists.items()}
# STEP: Define a new Editable graph with size updated.
g = nx.DiGraph()
g.add_nodes_from(self.editgraph.nodes)
g.add_edges_from(self.editgraph.edges)
self.canvas.axes.clear()
# WARNING : the definition of EditableGraph is the same as the definition in setup_design function. Any modification of one must be reported to the other.
self.editgraph = EditableGraph(g,
node_labels=node_labels, # node labels must be explicitly defined to avoid unwanted integer labeling.
node_color='antiquewhite',
node_edge_color='black',
node_label_fontdict=dict(family='sans-serif', color='black', weight='semibold', fontsize=11),
node_layout=positions,
node_size=self.designsize,
node_edge_width=self.designsize / 4,
node_label_offset=(0., 0.025 * self.designsize),
edge_width=self.designsize / 2,
arrows=True,
edge_labels=modules,
edge_label_position=0.75,
edge_label_fontdict=dict(family='sans-serif', fontweight='bold', fontsize=8, color='royalblue'),
edge_color=edge_color,
ax=self.canvas.axes)
# DEF: FILE MANAGEMENT
[docs]
def open(self):
"""Open the file dialog."""
filename = QFileDialog.getOpenFileName(self, "Open file", "", "Boon Files (*.boon);; All Files (*);;")
if filename:
self.filename = filename[0]
self.boon = BooN.load(filename[0])
self.refresh()
self.setup_design()
self.history_raz() # clear history
[docs]
def save(self):
"""Save file dialog."""
if self.filename:
self.boon.save(self.filename)
self.display_saved_flag() # update the saved flag
else:
self.saveas()
[docs]
def saveas(self):
"""Save as the file as dialog."""
filename = QFileDialog.getSaveFileName(self, "Save", "", "Boon Files (*.boon);; All Files (*);;")
if filename:
self.filename = filename[0]
self.boon.save(self.filename)
self.display_saved_flag() # update the saved flag
[docs]
def importation(self):
"""Import file dialog."""
filename = QFileDialog.getOpenFileName(self, "Import from files", "", "Text or SBML Files (*.bnet *.txt *.xml *.sbml);; All Files (*);;")
if filename:
self.filename = None # no file name since the BooN is not saved in the internal format.
_, extension = os.path.splitext(filename[0])
match extension: # Select the appropriate format to import a file
case ".bnet": # BoolNet Format
self.boon = BooN.from_textfile(filename[0])
case ".txt": # Python Format
self.boon = BooN.from_textfile(filename[0], sep=BOONSEP, assign='=', ops=SYMPY, skipline=PYTHONSKIP)
case ".sbml": # SBML Format
self.boon = BooN.from_sbmlfile(filename[0])
case ".xml": # SBML Format
self.boon = BooN.from_sbmlfile(filename[0])
case _:
QMessageBox.critical(self, "File extension error", f"The extension is unknown. \nFound {extension}\nAdmitted extension: .txt, .bnet, .sbml, .xml")
self.refresh()
self.setup_design()
self.history_raz() # clear history
[docs]
def exportation(self):
"""Export file dialog."""
filename = QFileDialog.getSaveFileName(self, "Export to BoolNet or Python format.", "", "Text Files (*.bnet *.txt);;")
if filename:
_, extension = os.path.splitext(filename[0])
match extension:
case ".bnet":
self.boon.to_textfile(filename[0])
case ".txt":
self.boon.to_textfile(filename[0], sep=BOONSEP, assign='=', ops=SYMPY, header=PYTHONHEADER)
[docs]
def quit(self):
"""Quit application."""
# check if the BooN is saved
if self.saved:
quitting = True
else: # Otherwise, ask whether it must be saved.
reply = QMessageBox.question(
self,
"Quit",
"Are you sure you want to quit? \nThe BooN is not saved.",
QMessageBox.Save | QMessageBox.Close | QMessageBox.Cancel,
QMessageBox.Save)
match reply:
case QMessageBox.Save:
self.save()
quitting = True
case QMessageBox.Close:
quitting = True
case QMessageBox.Cancel:
quitting = False
case _:
quitting = False
if quitting:
app.quit()
# noinspection PyMethodOverriding
[docs]
def closeEvent(self, event):
"""Close window"""
self.quit()
event.ignore() # WARNING: If the application is closed when quit() is triggered, this line will not be executed.
# DEF: HISTORY MANAGEMENT
[docs]
def history_raz(self):
"""Clear the history."""
self.history = [None] * HSIZE # History cleaning
self.hindex = 0 # Index of the last BooN added in the history
self.add_history() # Initialize the history with the current BooN
self.hupdate = False # Flag determining whether the history is updated
self.display_saved_flag() # The BooN is saved.
[docs]
def undo(self):
"""Undo operation."""
hindex = (self.hindex - 1) % HSIZE
if self.history[hindex]:
self.disablecallback = True # Prevent disruptive updates by disabling callback.
self.boon = self.history[hindex] # restore the previous BooN
self.hindex = hindex # update the history index
self.refresh()
self.setup_design()
[docs]
def redo(self):
"""Redo operation."""
hindex = (self.hindex + 1) % HSIZE
if self.history[hindex]:
self.disablecallback = True # Prevent disruptive updates by disabling callback.
self.boon = self.history[hindex].copy() # copy the current Boon
self.hindex = hindex
self.refresh()
self.setup_design()
[docs]
def add_history(self):
"""Add current BooN to the history."""
hindex = self.hindex
if self.boon != self.history[hindex]: # WARNING: The BooN comparison operates on descriptors only, not on positions.
self.disablecallback = True # Prevent disruptive updates by disabling callback.
self.hupdate = True # Descriptor is changed.
hindex = (hindex + 1) % HSIZE # Update the history
self.history[hindex] = self.boon.copy()
self.hindex = hindex
if self.history[hindex] and self.history[hindex].desc: # The current BooN is modified
self.display_saved_flag(False)
self.disablecallback = False # Enable the design callback
else:
self.hupdate = False # No changes.
# PRINT THE HISTORY
# self.show_history()
[docs]
def show_history(self):
"""Display the history. Used to debug the history management."""
view = [([i] if i == self.hindex else i,
tabulate([(var, logic.prettyform(eq, theboon.style)) for var, eq in theboon.desc.items()], tablefmt='plain')
if theboon else '-') for i, theboon in enumerate(self.history)]
os.system('cls')
print(tabulate(view, tablefmt='grid'))
[docs]
def refresh(self):
"""Refresh the opened widgets."""
if self.QView and self.QView.isVisible(): # Refresh the BooN View if opened.
self.QView.initialize_view()
if self.QStableStates and self.QStableStates.isVisible(): # Refresh the stable states View if opened.
self.QStableStates.stablestates()
if self.QModel and self.QModel.isVisible(): # Refresh the Model view if opened.
self.QModel.modeling()
if self.QControllability and self.QControllability.isVisible(): # Refresh the Controllability View if opened.
self.QControllability.initialize_controllability()
# DEF: WIDGETS OPENING
[docs]
def help(self):
"""Help View"""
thehelp = Help(self)
thehelp.show()
[docs]
def view(self):
"""BooN View"""
self.QView = View(self)
self.QView.show()
[docs]
def stablestates(self):
"""Stable States View"""
self.QStableStates = StableStates(self)
self.QStableStates.show()
[docs]
def model(self):
"""Model View"""
if len(self.boon.variables) > MODELBOUND:
QMessageBox.critical(self, "No Model", f"The number of variables exceeds {MODELBOUND}.\nThe model cannot be drawn.")
return
self.QModel = Model(self)
self.QModel.show()
[docs]
def controllability(self):
"""Controllability View"""
self.QControllability = Controllability(self)
self.QControllability.show()
def display_saved_flag(self, val: bool = True):
NOTSAVED: str = '\u2B24' # Large black circle
SAVED: str = '\u25CB' # Large empty circle
self.saved = val
if self.saved:
self.statusBar().showMessage(SAVED)
else:
self.statusBar().showMessage(NOTSAVED)
# DEF: WIDGET CLASSES
[docs]
class Help(QMainWindow):
"""Help Class used for showing help."""
def __init__(self, parent=None):
super(Help, self).__init__(parent)
loadUi('BooNGui/help.ui', self)
self.setMinimumSize(QSize(600, 600))
self.CloseButton.clicked.connect(lambda _: self.close())
self.web = QWebEngineView(self)
self.WebContainer.addWidget(self.web)
with open('BooNGui/Help.html', 'r') as f:
html = f.read()
self.web.setHtml(html)
[docs]
class View(QDialog):
"""View Class used for showing the BooN formula in a table."""
def __init__(self, parent=None):
super(View, self).__init__(parent)
loadUi('BooNGui/view.ui', self)
self.setGeometry(300, 300, 750, 500)
self.style = LOGICAL # Style of the formulas
self.parent = parent # parent = Boonify class
self.formulas = None # formulas of BooN
# STEP: Set the functions related to signals
self.CloseButton.clicked.connect(lambda _: self.close())
self.DnfButton.clicked.connect(self.convertdnf)
# STEP: Combox Box of style.
self.Style.activated.connect(self.cb_styling)
# STEP: Forbid the edition of BooNContent.
self.BooNContent.setEditTriggers(QtWidgets.QTableWidget.NoEditTriggers)
# STEP: Resize columns of the table to content.
self.BooNContent.setColumnWidth(1, 500)
# STEP: Fix size of the formula columns.
header = self.BooNContent.horizontalHeader()
header.setSectionResizeMode(QHeaderView.Stretch)
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.ResizeToContents)
header.setStretchLastSection(True)
header.setSectionResizeMode(2, QHeaderView.Interactive)
self.initialize_view()
[docs]
def initialize_view(self):
"""Fill the table."""
theboon = self.parent.boon
# STEP: Initialize the formula fields.
nbrow = len(theboon.desc)
self.BooNContent.setRowCount(nbrow)
self.formulas = [QLineEdit() for _ in range(nbrow)]
for f in self.formulas:
f.editingFinished.connect(self.change_formula)
f.setFrame(False)
# STEP: Fill the table
for row, var in enumerate(theboon.desc):
item = QTableWidgetItem(str(var))
item.setTextAlignment(Qt.AlignCenter)
self.BooNContent.setItem(row, 1, item) # variable
self.formulas[row].setText(logic.prettyform(theboon.desc[var], style=self.style))
self.BooNContent.setCellWidget(row, 2, self.formulas[row]) # formula
if is_dnf(theboon.desc[var]): # type of the formula
form = "DNF"
elif is_cnf(theboon.desc[var]):
form = "CNF"
elif is_nnf(theboon.desc[var]):
form = "NNF"
else:
form = "ALL"
item = QTableWidgetItem(form)
item.setTextAlignment(Qt.AlignHCenter)
self.BooNContent.setItem(row, 0, item)
[docs]
def cb_styling(self):
"""Style from combo box selection"""
self.style = STYLE[self.Style.currentText()]
self.initialize_view() # Refresh the view.
[docs]
def convertdnf(self):
"""Convert the BooN into DNF."""
self.parent.boon.dnf()
self.initialize_view() # Refresh the view.
[docs]
class StableStates(QDialog):
"""Class to handle stable states computation."""
def __init__(self, parent=None):
super(StableStates, self).__init__(parent)
loadUi('BooNGui/stablestates.ui', self)
self.setGeometry(300, 300, 500, 700)
self.parent = parent
self.style = 'Icon Boolean'
self.datamodel = None
# Button and Combo Box
self.CloseButton.clicked.connect(lambda _: self.close())
self.Style.activated.connect(self.cb_styling)
# shrink the header to size
Hheader = self.StableStatesPanel.horizontalHeader()
Hheader.setSectionResizeMode(QHeaderView.ResizeToContents)
self.stablestates()
[docs]
def cb_styling(self):
"""Select style from combo box selection"""
self.style = self.Style.currentText()
self.stablestates() # Refresh the stable states view.
[docs]
def stablestates(self):
"""Compute stable states and arrange the view."""
theboon = self.parent.boon
variables = theboon.variables
stablestates = theboon.stable_states
# Define a model of data to store stable states.
self.datamodel = QStandardItemModel()
self.datamodel.setRowCount(len(variables))
self.datamodel.setVerticalHeaderLabels([str(var) for var in variables])
# Fill the table.
for stable in stablestates:
column = []
for var in variables:
icon = QIcon()
icon.addPixmap(QtGui.QPixmap(ICON01[stable[var]]), QtGui.QIcon.Normal, QtGui.QIcon.Off)
icon.pixmap(QSize(64, 64))
match self.style: # define the view from the style
case 'Icon':
item = QStandardItem(icon, "")
case 'Icon Boolean':
item = QStandardItem(icon, str(stable[var]))
case 'Icon 0-1':
item = QStandardItem(icon, str(int(stable[var])))
case 'Boolean':
item = QStandardItem(str(stable[var]))
case '0-1':
item = QStandardItem(str(int(stable[var])))
case _:
item = QStandardItem("None")
item.setTextAlignment(Qt.AlignCenter)
column.append(item)
self.datamodel.appendColumn(column)
self.StableStatesPanel.setModel(self.datamodel)
[docs]
class Model(QMainWindow):
"""Class to handle model of dynamics."""
def __init__(self, parent=None):
super(Model, self).__init__(parent)
loadUi('BooNGui/model.ui', self)
self.setGeometry(300, 300, 600, 600)
self.CloseButton.clicked.connect(lambda _: self.close())
# STEP: initialize attributes
self.parent = parent
self.mode = boon.asynchronous
self.layout = boon.hypercube_layout
# STEP: Connect Matplotlib widget
self.canvas = FigureCanvas(Figure())
self.ModelCanvas.addWidget(self.canvas)
self.canvas.axes = self.canvas.figure.add_subplot(111)
# STEP: connect functions to signals.
# Radio Button to select the mode
self.AsynchronousButton.clicked.connect(self.rb_mode)
self.SynchronousButton.clicked.connect(self.rb_mode)
# Network layout Combo box
self.NetworkLayout.activated.connect(self.cb_network_layout)
# STEP: Modeling
self.modeling()
[docs]
def rb_mode(self):
"""Mode selection from radio button box."""
if self.AsynchronousButton.isChecked():
self.mode = boon.asynchronous
elif self.SynchronousButton.isChecked():
self.mode = boon.synchronous
else:
pass
self.modeling()
[docs]
def cb_network_layout(self):
"""Determine the layout of the network model from the combo box."""
layout = self.NetworkLayout.currentText()
match layout:
case "Hypercube":
self.layout = boon.hypercube_layout
case "Circular":
self.layout = nx.circular_layout
case "Spring":
self.layout = nx.spring_layout
case "Kamada Kawai":
self.layout = nx.kamada_kawai_layout
case "Random":
self.layout = nx.random_layout
case _:
logic.errmsg("Internal Error - Unknown layout - Please contact the development team", "cb_network_layout")
self.modeling()
[docs]
def modeling(self):
"""Compute the model of dynamics."""
self.canvas.axes.clear()
self.canvas.axes.axis('off')
model = self.parent.boon.model(mode=self.mode)
if model.number_of_nodes() == 0: # Empty datamodel = empty BooN
return
layout = self.layout(model)
self.parent.boon.draw_model(model, pos=layout, ax=self.canvas.axes)
self.canvas.draw_idle()
[docs]
class Controllability(QMainWindow):
"""Controllability class"""
def __init__(self, parent=None):
super(Controllability, self).__init__(parent)
loadUi('BooNGui/controllability.ui', self)
self.setGeometry(900, 300, 800, 600)
self.parent = parent
self.actions = None # current control actions
self.row = None # row number corresponding to the selected solutions
self.initialize_controllability()
[docs]
def initialize_controllability(self):
"""Initialize the controllability widget."""
theboon = self.parent.boon
variables = theboon.variables
nbrow = len(theboon.desc)
# STEP : set the controllability as the function of the thread
self.parent.worker.apply(self.controllability)
# STEP: Initialize Destiny page
self.Destiny.setRowCount(nbrow)
self.Destiny.resizeColumnToContents(0) # fit size to content
self.Destiny.horizontalHeader().setStretchLastSection(True)
# fill the destiny table
for row, var in enumerate(variables):
# Add variable name
item = QTableWidgetItem(str(var))
item.setTextAlignment(Qt.AlignCenter)
self.Destiny.setItem(row, 0, item)
# Add a status for each variable
statusbox = QComboBox(self)
# item of the status box
statusbox.addItems(["None", "True", "False"]) # possible status
# insert icons for each status
icon = QIcon(ICON01[None]) # icon for None
icon.pixmap(QSize(64, 64))
statusbox.setItemIcon(0, icon)
icon = QIcon(ICON01[True]) # icon for True
icon.pixmap(QSize(64, 64))
statusbox.setItemIcon(1, icon)
icon = QIcon(ICON01[False]) # icon for False
icon.pixmap(QSize(64, 64))
statusbox.setItemIcon(2, icon)
# insert the status box in the table and connect it
self.Destiny.setCellWidget(row, 1, statusbox)
statusbox.currentTextChanged.connect(self.destiny_to_observers)
# STEP: Initialize the observer page
self.Observers.setRowCount(nbrow)
self.Observers.horizontalHeader().setStretchLastSection(True)
# fill the observer table
for row, var in enumerate(variables):
obschkbox = QTableWidgetItem(str(var)) # add checkbox
obschkbox.setText(str(var))
obschkbox.setFlags(Qt.ItemIsUserCheckable | Qt.ItemIsEnabled)
obschkbox.setCheckState(Qt.Unchecked)
self.Observers.setItem(row, 0, obschkbox)
# STEP: Define signals
self.Observers.itemClicked.connect(self.observers_to_destiny)
self.ControlButton.clicked.connect(self.parent.worker.run)
self.ControlActions.clicked.connect(self.select_action)
self.ActButton.clicked.connect(self.actupon)
# STEP: Set the destiny page as default
self.ControlPanel.setCurrentIndex(0)
# STEP: Set size of columns
# fit to content for ControlPanel
for i in range(self.ControlPanel.count()):
self.ControlPanel.widget(i).adjustSize()
# fit to content for ControlActions
header = self.ControlActions.header()
header.setSectionResizeMode(QHeaderView.ResizeToContents)
header.setStretchLastSection(True)
[docs]
def destiny_to_observers(self, label: str):
"""Modify the observer w.r.t. the destiny change."""
row = self.Destiny.currentRow()
item = self.Observers.item(row, 0)
if label == "None":
item.setCheckState(Qt.Unchecked)
else:
item.setCheckState(Qt.Checked)
[docs]
def observers_to_destiny(self, chkitem):
"""Modify the destiny w.r.t. the observer change"""
row = chkitem.row()
if chkitem.checkState() == Qt.Unchecked:
combobox = self.Destiny.cellWidget(row, 1)
combobox.setCurrentText("None")
[docs]
def controllability(self):
"""Compute the control actions based on the destiny and the observers."""
self.row = None
theboon = self.parent.boon
variables = list(theboon.variables)
# STEP: Get the observers
controlledvars = set()
for row in range(self.Observers.rowCount()):
item = self.Observers.item(row, 0)
if item.checkState() == Qt.Checked:
pass
else:
controlledvars.add(variables[row])
# STEP: Get the query formula
query = {}
for row in range(self.Destiny.rowCount()):
combobox = self.Destiny.cellWidget(row, 1)
match combobox.currentText():
case "None":
pass
case "True":
query.update({variables[row]: True})
case "False":
query.update({variables[row]: False})
# STEP: Convert the state profiles into minterm
formula = SOPform(query.keys(), [query])
# Check whether the query must be reached or avoid.
match self.QueryType.currentText():
case "Reach":
pass
case "Avoid":
formula = Not(formula)
# STEP: Add control
boonctrl = theboon.copy()
boonctrl.control(controlledvars, controlledvars)
# STEP: Interpret the modality of the query (possibility, necessity).
if self.Possibility.isChecked(): # Possibility
possibility = boonctrl.possibly(formula)
else:
possibility = True
if self.Necessity.isChecked(): # Necessity
necessity = boonctrl.necessary(formula, trace=self.Trace)
else:
necessity = True
destiny = And(possibility, necessity)
# STEP: Destify the controlled BooN and transform the solutions into control actions (var, Boolean Value)
core = BooN.destify(destiny, trace=self.Trace)
self.actions = boon.core2actions(core)
# STEP: Define the tree model to show the resulting actions.
treemodel = QStandardItemModel(0, 2) # Add 2 columns
treemodel.setHeaderData(0, Qt.Horizontal, "Variable")
treemodel.setHeaderData(1, Qt.Horizontal, "Boolean value")
root = treemodel.invisibleRootItem()
match self.actions:
case []: # No actions
item = QStandardItem("No action found.")
root.appendRow(item)
case [[]]: # The destiny profile is already obtained
item = QStandardItem("The marking profile already exists.")
root.appendRow(item)
case _: # Compute the control actions.
for i, actions in enumerate(self.actions, 1):
# create the root solution
rootactions = QStandardItem("Solution {:2d}".format(i))
# add the control actions of this solution
for action in actions:
# a control action is displayed as: variable + Boolean icon + Boolean value
variable = QStandardItem(str(action[0]))
icon = QIcon(ICON01[action[1]])
icon.pixmap(QSize(64, 64))
value = QStandardItem(icon, str(action[1]))
rootactions.appendRow([variable, value])
# append the solution to the tree model
root.appendRow(rootactions)
self.ControlActions.setModel(treemodel) # set the data model to tree widget enabling its display.
self.ControlActions.expandAll()
[docs]
def select_action(self, arg):
"""Keep the selection solution"""
self.row = arg.parent().row() if arg.parent().row() > -1 else arg.row()
[docs]
def actupon(self):
"""Apply the selection actions on the BooN."""
if self.row is not None and self.actions:
for action in self.actions[self.row]:
(variable, value) = action
self.parent.boon.desc[variable] = value
self.parent.add_history()
self.parent.setup_design()
self.parent.refresh()
self.close()
[docs]
class Threader(QObject):
"""Class executing a thread to run an application."""
finished = pyqtSignal()
def __init__(self, app=lambda: None):
super().__init__()
self.app = app
# Create the thread
self.thread = QThread()
self.moveToThread(self.thread)
self.thread.start()
[docs]
def run(self):
"""run the application"""
self.app() # run the application
self.finished.emit() # Emit the end signal
[docs]
def apply(self, app):
"""Fix the application running in the thread."""
self.app = app
[docs]
def quit(self):
"""terminate the thread"""
self.thread.quit()
# DEF: MAIN
if __name__ == "__main__":
app = QApplication(sys.argv)
boonify = Boonify()
callback = boonify.canvas.mpl_connect('draw_event', lambda _: boonify.design())
boonify.show()
sys.exit(app.exec_())