#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""This module contains the actual Qt app."""
from PyQt5 import QtWidgets, QtCore
from PyQt5.Qt import QRect
import sys
import logging
from AquaponicsModeler import interface
from AquaponicsModeler import model
from AquaponicsModeler.plotcanvas import PlotWindow
from AquaponicsModeler.plotdata import DataSeries, DataItem, PlotData
logging.basicConfig(level=logging.INFO, stream=sys.stdout)
[docs]def run_model(chain, timestep, runtime, log):
"""
Run the actual model and return the data series.
Args:
timestep (int): The time steps (seconds) for the model
Minimum is 1 second, max is 3600s (1 hour)
runtime (int): The total time (hours) the model should run for
Minimum is 1 hour, maximum is 23 hours
chain (list): A list of model in the order they are
connected in the model. The componets should be instances of
classes inheriting from :class:`<model.BaseModelCompontent>`
log (logging.Logger): The logger to use for logging
Returns:
list: A list with an item for the time axis plus an item for
each model component in chain, with the data series for that
component.
"""
runtime = runtime * 3600
results = [[] for c in chain]
results.append([])
for t in range(0, runtime, timestep):
results[0].append(t)
log.debug("Stepping to time %i" % t)
for i, c in enumerate(chain):
results[i + 1].append(c.get_state())
for c in reversed(chain):
c.step(timestep)
return results
[docs]class AquaponicsModeler(QtWidgets.QMainWindow, interface.Ui_MainWindow):
"""The graphical interface to the aquaponics modeler."""
def __init__(self, log, parent=None):
"""
Instantiate the application.
Args:
log (logging.Logger): The logger to send log messages to.
parent (PyQt5.Qt.QWidget): The parent widget.
"""
super(AquaponicsModeler, self).__init__(parent)
self.log = log
self.setupUi(self)
self.bt_add_row.clicked.connect(self.addRow)
self.bt_del_row.clicked.connect(self.deleteSelectedRows)
self.btn_run.clicked.connect(self.runModel)
self.plotWindow = PlotWindow()
self.plotWindow.setGeometry(QRect(0, 0, 600, 400))
[docs] def showErrorMessage(self, message):
"""
Popup a message box with an error message.
Args:
message (str): The message to display.
"""
msgbx = QtWidgets.QMessageBox()
msgbx.setText(message)
msgbx.setIcon(QtWidgets.QMessageBox.Critical)
msgbx.exec_()
[docs] def plotResults(self, results, chain):
"""
Plot the results of a model run with matplotlib.
results (list): A list of lists. The first item is the x-axis
(time component). Subsequent items are series for each model
component in the order in which they are in the chain. Each of
these lists contains the state values of the component for each
step through the model.
chain (list): The model chain, which is a list of instances
inheriting from :class:`model.BaseModelClass`.
"""
x = [r / 60 for r in results[0]]
pumpSeries = DataSeries(x_title='Time (min)',
y_title='State (on/off)',
x_values=x, dataItems=[])
containerSeries = DataSeries(x_title='Time (min)',
y_title='Contents (L)',
x_values=x, dataItems=[])
self.log.debug("Got %i items for time axis" % len(x))
items = []
names = []
for i, c in enumerate(chain):
params = c.__class__.getParameters()
paramValues = [(v[1], str(getattr(c, k))) for k,v in params.items()]
num = 1
name = "%s%i" % (c, num)
while name in names:
num += 1
name = "%s%i" % (c, num)
names.append(name)
item = DataItem(title=name,
values=results[i + 1],
params=paramValues)
if isinstance(c, model.Pump):
self.log.debug('Adding a series with %i items to pumpSeries.'
% len(item.values))
pumpSeries.dataItems.append(item)
elif isinstance(c, model.SimpleContainer):
self.log.debug('Adding a series with %i' % len(item.values) +
' items to containerSeries.')
containerSeries.dataItems.append(item)
items.append(item)
desc = "Model components: %s" % ", ".join(
["%s (%s)" % (i.title, ','.join(
["%s: %s" %(k, v) for k, v in i.params])) for i in items])
dataSeries = []
if(len(containerSeries.dataItems)>0):
dataSeries.append(containerSeries)
if(len(pumpSeries.dataItems)>0):
dataSeries.append(pumpSeries)
data = PlotData(dataSeries=dataSeries,
description=desc)
self.plotWindow.plot(data)
self.plotWindow.show()
[docs] def runModel(self):
"""Run the actual model."""
chain = []
for i in range(0, self.modelLayout.count()):
row = self.modelLayout.itemAt(i).widget().layout()
typewidget = row.itemAt(1).widget()
componentType = typewidget.itemData(typewidget.currentIndex())
if componentType is None:
self.showErrorMessage("The component in row %i doesn't have " +
"a type yet. Please select one." %
(i + 1))
return
self.log.debug('Component %i is of type %s' %
(i, componentType.__name__))
values = {}
params = componentType.getParameters()
if 'previous' in params.keys():
if i == 0:
self.showErrorMessage("The %s component at row %i can't be"
"the first component. It needs a "
"source of water. Try adding a pump."
% (componentType.__name__, (i + 1)))
return
else:
values['previous'] = chain[i - 1]
for j in range(2, row.count()):
w = row.itemAt(j).widget()
name = w.objectName()
self.log.debug('Checking widget %s' % name)
if name[:2] == 'w_':
name = name[2:]
if hasattr(w, 'value'):
v = w.value()
elif hasattr(w, 'text'):
v = w.text()
else:
self.log.error("Unable to fetch value for widget %s %s"
% (name, w))
v = None
values[name] = v
self.log.debug('Got parameter values %s' % values)
component = componentType(**values)
chain.append(component)
if len(chain) == 0:
self.showErrorMessage("No model components have been defined "
"yet. Please add rows.")
return
self.log.debug('Got chain %s' % chain)
timestep = self.inp_timestep.value()
runtime = self.inp_runtime.value()
self.log.debug('Got timestep %i and runtime %i' % (timestep, runtime))
results = run_model(chain, timestep, runtime, self.log)
self.plotResults(results, chain)
[docs] def addRow(self):
"""Add a row to the list of components."""
count = self.modelLayout.count() # The number of rows already there
"""The initial widgets (dropbox for component type and checkbox
to select the row"""
checkbox = QtWidgets.QCheckBox()
typebox = QtWidgets.QComboBox()
typebox.addItem("-- choose a component--", None)
typebox.currentIndexChanged.connect(self.updateModelComponent)
for i, t in enumerate(model.get_components()):
typebox.addItem(t.__name__, t)
# The frame widget and layout holding this row.
frame = QtWidgets.QFrame(self.modelFrame)
frame.setFrameShape(QtWidgets.QFrame.StyledPanel)
frame.setFrameShadow(QtWidgets.QFrame.Raised)
frame.setObjectName("componentFrame%i" % count)
modelComponent = QtWidgets.QHBoxLayout(frame)
modelComponent.setAlignment(QtCore.Qt.AlignLeft)
modelComponent.setObjectName("modelComponent%i" % count)
# Add the component widgets to the frame
modelComponent.addWidget(checkbox)
modelComponent.addWidget(typebox)
# Add the frame to the application
self.modelLayout.addWidget(frame)
[docs] def updateModelComponent(self):
"""
Action to perform when the component type dropbox changes.
It removes all parameter widgets for the respective row and adds the
parameter widgets for the new component type.
"""
sender = self.sender()
layout = sender.parent().layout()
index = layout.indexOf(sender)
rem_items = []
for i in range(index + 1, layout.count()):
"""First loop over the widgets before removing them as the index
will change once removed."""
item = layout.itemAt(i)
if item is None:
continue
self.log.debug('item at at %s: %s' % (i, item.widget()))
rem_items.append(item)
for i in rem_items:
# Now remove the items.
layout.removeItem(i)
widget = i.widget()
widget.deleteLater()
# get the component type
component = sender.itemData(sender.currentIndex())
self.log.debug('Changed model component to %s' % component)
# get the model parameters for the type
params = component.getParameters()
for k, v in params.items():
# add all parameters to the layout
if k == 'previous':
continue
else:
for w in get_parameter_widget(k, v):
layout.addWidget(w)
self.log.debug('adding widget %s %s' % (k, w))
[docs] def deleteSelectedRows(self):
"""
Action to perform when the remove row button is clicked.
All component rows for which the checkbox was clicked are removed.
"""
layout = self.modelLayout
self.log.debug('Number of component rows: %s' % (layout.count()))
remove_rows = []
for r in range(0, layout.count()):
row_item = layout.itemAt(r)
if row_item is None:
self.log.warn('Widget at row %s is of type None' % r)
continue
row = row_item.widget()
self.log.debug('Item at row %s: %s' % (r, row))
componentLayout = row.layout()
item = componentLayout.itemAt(0)
if item is None:
self.log.debug('Widget at row %s and position 0 is None' % r)
continue
widget = item.widget()
self.log.debug('Widget at row %s and position 0: %s' %
(r, widget))
if isinstance(widget, QtWidgets.QCheckBox) and widget.isChecked():
self.log.debug('Removing widgets for row %s' % r)
remove_rows.append(row_item)
items = [componentLayout.itemAt(c)
for c in range(0, componentLayout.count())]
for c, item in enumerate(items):
if item is None:
continue
self.log.debug('item at at %s %s: %s' %
(r, c, item.widget()))
componentLayout.removeItem(item)
widget = item.widget()
widget.deleteLater()
else:
self.log.debug('Leaving row %s alone' % r)
for row in remove_rows:
"""Remove the actual rows (Frames that used to contain the widgets
for the component)"""
self.log.debug('Removing row %s' % row.widget())
layout.removeItem(row)
row.widget().deleteLater()
self.log.debug('Rows remaning: %i' % layout.count())
[docs]def main():
"""Run the actual application."""
log = logging.getLogger("aquaponics.interface")
app = QtWidgets.QApplication(sys.argv)
form = AquaponicsModeler(log)
form.show()
app.exec_()
if __name__ == '__main__':
main()
__all__ = ['AquaponicsModeler', 'run_model', 'get_parameter_widget', 'main']