freecad-cam/Mod/Fem/femsolver/solver_taskpanel.py
2026-02-01 01:59:24 +01:00

340 lines
12 KiB
Python

# ***************************************************************************
# * Copyright (c) 2017 Markus Hovorka <m.hovorka@live.de> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * This program is free software; you can redistribute it and/or modify *
# * it under the terms of the GNU Lesser General Public License (LGPL) *
# * as published by the Free Software Foundation; either version 2 of *
# * the License, or (at your option) any later version. *
# * for detail see the LICENCE text file. *
# * *
# * This program 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. See the *
# * GNU Library General Public License for more details. *
# * *
# * You should have received a copy of the GNU Library General Public *
# * License along with this program; if not, write to the Free Software *
# * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 *
# * USA *
# * *
# ***************************************************************************
__title__ = "FreeCAD FEM solver job control task panel"
__author__ = "Markus Hovorka"
__url__ = "https://www.freecad.org"
## \addtogroup FEM
# @{
from PySide import QtCore
from PySide import QtGui
import FreeCADGui as Gui
import femsolver.report
import femsolver.run
_UPDATE_INTERVAL = 50
_REPORT_TITLE = "Run Report"
_REPORT_ERR = "Failed to run. Please try again after all of the following errors are resolved."
class ControlTaskPanel(QtCore.QObject):
machineChanged = QtCore.Signal(object)
machineStarted = QtCore.Signal(object)
machineStopped = QtCore.Signal(object)
machineStatusChanged = QtCore.Signal(str)
machineStatusCleared = QtCore.Signal()
machineTimeChanged = QtCore.Signal(float)
machineStateChanged = QtCore.Signal(float)
def __init__(self, machine):
super().__init__()
self.form = ControlWidget()
self._machine = None
# Timer that updates the duration indicator.
self._timer = QtCore.QTimer()
self._timer.setInterval(_UPDATE_INTERVAL)
self._timer.timeout.connect(self._timeProxy)
# Connect object to widget.
self.form.writeClicked.connect(self.write)
self.form.editClicked.connect(self.edit)
self.form.runClicked.connect(self.run)
self.form.abortClicked.connect(self.abort)
self.form.directoryChanged.connect(self.updateMachine)
# Seems that the task panel does not get destroyed. Disconnect
# as soon as the widget of the task panel gets destroyed.
self.form.destroyed.connect(self._disconnectMachine)
self.form.destroyed.connect(self._timer.stop)
# self.form.destroyed.connect(
# lambda: self.machineStatusChanged.disconnect(
# self.form.appendStatus))
# Connect all proxy signals.
self.machineStarted.connect(self._timer.start)
self.machineStarted.connect(self.form.updateState)
self.machineStopped.connect(self._timer.stop)
self.machineStopped.connect(self._displayReport)
self.machineStopped.connect(self.form.updateState)
self.machineStatusChanged.connect(self.form.appendStatus)
self.machineStatusCleared.connect(self.form.clearStatus)
self.machineTimeChanged.connect(self.form.setTime)
self.machineStateChanged.connect(lambda: self.form.updateState(self.machine))
self.machineChanged.connect(self._updateTimer)
# Set initial machine. Signal updates the widget.
self.machineChanged.connect(self.updateWidget)
self.form.destroyed.connect(lambda: self.machineChanged.disconnect(self.updateWidget))
self.machine = machine
@property
def machine(self):
return self._machine
@machine.setter
def machine(self, value):
self._connectMachine(value)
self._machine = value
self.machineChanged.emit(value)
@QtCore.Slot()
def write(self):
self.machine.reset()
self.machine.target = femsolver.run.PREPARE
self.machine.start()
@QtCore.Slot()
def run(self):
self.machine.reset(femsolver.run.SOLVE)
self.machine.target = femsolver.run.RESULTS
self.machine.start()
@QtCore.Slot()
def edit(self):
self.machine.reset(femsolver.run.SOLVE)
self.machine.solver.Proxy.edit(self.machine.directory)
@QtCore.Slot()
def abort(self):
self.machine.abort()
@QtCore.Slot()
def updateWidget(self):
self.form.setDirectory(self.machine.directory)
self.form.setStatus(self.machine.status)
self.form.setTime(self.machine.time)
self.form.updateState(self.machine)
@QtCore.Slot()
def updateMachine(self):
if self.form.directory() != self.machine.directory:
self.machine = femsolver.run.getMachine(self.machine.solver, self.form.directory())
@QtCore.Slot()
def _updateTimer(self):
if self.machine.running:
self._timer.start()
@QtCore.Slot(object)
def _displayReport(self, machine):
text = _REPORT_ERR if machine.failed else None
femsolver.report.display(machine.report, _REPORT_TITLE, text)
def getStandardButtons(self):
return QtGui.QDialogButtonBox.Close
def reject(self):
Gui.ActiveDocument.resetEdit()
def _connectMachine(self, machine):
self._disconnectMachine()
machine.signalStatus.add(self._statusProxy)
machine.signalStatusCleared.add(self._statusClearedProxy)
machine.signalStarted.add(self._startedProxy)
machine.signalStopped.add(self._stoppedProxy)
machine.signalState.add(self._stateProxy)
def _disconnectMachine(self):
if self.machine is not None:
self.machine.signalStatus.remove(self._statusProxy)
self.machine.signalStatusCleared.add(self._statusClearedProxy)
self.machine.signalStarted.remove(self._startedProxy)
self.machine.signalStopped.remove(self._stoppedProxy)
self.machine.signalState.remove(self._stateProxy)
def _startedProxy(self):
self.machineStarted.emit(self.machine)
def _stoppedProxy(self):
self.machineStopped.emit(self.machine)
def _statusProxy(self, line):
self.machineStatusChanged.emit(line)
def _statusClearedProxy(self):
self.machineStatusCleared.emit()
def _timeProxy(self):
time = self.machine.time
self.machineTimeChanged.emit(time)
def _stateProxy(self):
state = self.machine.state
self.machineStateChanged.emit(state)
class ControlWidget(QtGui.QWidget):
writeClicked = QtCore.Signal()
editClicked = QtCore.Signal()
runClicked = QtCore.Signal()
abortClicked = QtCore.Signal()
directoryChanged = QtCore.Signal()
def __init__(self, parent=None):
super().__init__(parent)
self._setupUi()
self._inputFileName = ""
def _setupUi(self):
self.setWindowTitle(self.tr("Solver Control"))
# Working directory group box
self._directoryTxt = QtGui.QLineEdit()
self._directoryTxt.editingFinished.connect(self.directoryChanged)
directoryBtt = QtGui.QToolButton()
directoryBtt.setText("...")
directoryBtt.clicked.connect(self._selectDirectory)
directoryLyt = QtGui.QHBoxLayout()
directoryLyt.addWidget(self._directoryTxt)
directoryLyt.addWidget(directoryBtt)
self._directoryGrp = QtGui.QGroupBox()
self._directoryGrp.setTitle(self.tr("Working Directory"))
self._directoryGrp.setLayout(directoryLyt)
# Action buttons (Write, Edit, Run)
self._writeBtt = QtGui.QPushButton(self.tr("Write"))
self._editBtt = QtGui.QPushButton(self.tr("Edit"))
self._runBtt = QtGui.QPushButton()
self._writeBtt.clicked.connect(self.writeClicked)
self._editBtt.clicked.connect(self.editClicked)
actionLyt = QtGui.QGridLayout()
actionLyt.addWidget(self._writeBtt, 0, 0)
actionLyt.addWidget(self._editBtt, 0, 1)
actionLyt.addWidget(self._runBtt, 1, 0, 1, 2)
# Solver status log
self._statusEdt = QtGui.QPlainTextEdit()
self._statusEdt.setReadOnly(True)
# for the log we need a certain height
# set it so to almost match the size of the CCX solver panel
self._statusEdt.setMinimumHeight(300)
# Elapsed time indicator
timeHeaderLbl = QtGui.QLabel(self.tr("Elapsed Time:"))
self._timeLbl = QtGui.QLabel()
timeLyt = QtGui.QHBoxLayout()
timeLyt.addWidget(timeHeaderLbl)
timeLyt.addWidget(self._timeLbl)
timeLyt.addStretch()
timeLyt.setContentsMargins(0, 0, 0, 0)
self._timeWid = QtGui.QWidget()
self._timeWid.setLayout(timeLyt)
# Main layout
layout = QtGui.QVBoxLayout()
layout.addWidget(self._directoryGrp)
layout.addLayout(actionLyt)
layout.addWidget(self._statusEdt)
layout.addWidget(self._timeWid)
self.setLayout(layout)
@QtCore.Slot(str)
def setStatus(self, text):
if text is None:
text = ""
self._statusEdt.setPlainText(text)
self._statusEdt.moveCursor(QtGui.QTextCursor.End)
def status(self):
return self._statusEdt.plainText()
@QtCore.Slot(str)
def appendStatus(self, line):
self._statusEdt.moveCursor(QtGui.QTextCursor.End)
self._statusEdt.insertPlainText(line)
self._statusEdt.moveCursor(QtGui.QTextCursor.End)
@QtCore.Slot(str)
def clearStatus(self):
self._statusEdt.setPlainText("")
@QtCore.Slot(float)
def setTime(self, time):
timeStr = "<b>%05.1f</b>" % time if time is not None else ""
self._timeLbl.setText(timeStr)
def time(self):
if self._timeLbl.text() == "":
return None
return float(self._timeLbl.text())
@QtCore.Slot(float)
def setDirectory(self, directory):
self._directoryTxt.setText(directory)
def directory(self):
return self._directoryTxt.text()
@QtCore.Slot(int)
def updateState(self, machine):
if machine.state <= femsolver.run.PREPARE:
self._writeBtt.setText(self.tr("Write"))
self._editBtt.setText(self.tr("Edit"))
self._runBtt.setText(self.tr("Run"))
elif machine.state <= femsolver.run.SOLVE:
self._writeBtt.setText(self.tr("Re-write"))
self._editBtt.setText(self.tr("Edit"))
self._runBtt.setText(self.tr("Run"))
else:
self._writeBtt.setText(self.tr("Re-write"))
self._editBtt.setText(self.tr("Edit"))
self._runBtt.setText(self.tr("Re-run"))
if machine.running:
self._runBtt.setText(self.tr("Abort"))
self.setRunning(machine)
@QtCore.Slot()
def _selectDirectory(self):
path = QtGui.QFileDialog.getExistingDirectory(self)
self.setDirectory(path)
self.directoryChanged.emit()
def setRunning(self, machine):
if machine.running:
self._runBtt.clicked.connect(self.runClicked)
self._runBtt.clicked.disconnect()
self._runBtt.clicked.connect(self.abortClicked)
self._directoryGrp.setDisabled(True)
self._writeBtt.setDisabled(True)
self._editBtt.setDisabled(True)
else:
self._runBtt.clicked.connect(self.abortClicked)
self._runBtt.clicked.disconnect()
self._runBtt.clicked.connect(self.runClicked)
self._directoryGrp.setDisabled(False)
self._writeBtt.setDisabled(False)
self._editBtt.setDisabled(
not machine.solver.Proxy.editSupported() or machine.state <= femsolver.run.PREPARE
)
## @}