FEM: new solver framework, initial commit
This commit is contained in:
@@ -118,6 +118,17 @@ SET(FemObjectsScripts_SRCS
|
||||
PyObjects/_FemMaterial.py
|
||||
)
|
||||
|
||||
SET(FemSolver_SRCS
|
||||
femsolver/__init__.py
|
||||
femsolver/solverbase.py
|
||||
femsolver/report.py
|
||||
femsolver/reportdialog.py
|
||||
femsolver/settings.py
|
||||
femsolver/task.py
|
||||
femsolver/run.py
|
||||
femsolver/signal.py
|
||||
)
|
||||
|
||||
SET(FemGuiScripts_SRCS
|
||||
PyGui/FemCommands.py
|
||||
PyGui/FemSelectionObserver.py
|
||||
@@ -154,6 +165,7 @@ SET(FemGuiScripts_SRCS
|
||||
PyGui/_TaskPanelFemMeshRegion.py
|
||||
PyGui/_TaskPanelFemResultShow.py
|
||||
PyGui/_TaskPanelFemSolverCalculix.py
|
||||
PyGui/_TaskPanelFemSolverControl.py
|
||||
PyGui/_ViewProviderFemConstraintSelfWeight.py
|
||||
PyGui/_ViewProviderFemElementFluid1D.py
|
||||
PyGui/_ViewProviderFemElementGeometry1D.py
|
||||
@@ -319,7 +331,8 @@ fc_target_copy_resource(Fem
|
||||
${FemObjectsScripts_SRCS}
|
||||
${FemGuiScripts_SRCS}
|
||||
${FemTests_SRCS}
|
||||
)
|
||||
${FemSolver_SRCS}
|
||||
)
|
||||
|
||||
SET_BIN_DIR(Fem Fem /Mod/Fem)
|
||||
SET_PYTHON_PREFIX_SUFFIX(Fem)
|
||||
|
||||
@@ -70,6 +70,20 @@ INSTALL(
|
||||
Mod/Fem/PyObjects
|
||||
)
|
||||
|
||||
INSTALL(
|
||||
FILES
|
||||
femsolver/__init__.py
|
||||
femsolver/solverbase.py
|
||||
femsolver/report.py
|
||||
femsolver/reportdialog.py
|
||||
femsolver/settings.py
|
||||
femsolver/task.py
|
||||
femsolver/run.py
|
||||
femsolver/signal.py
|
||||
DESTINATION
|
||||
Mod/Fem/femsolver
|
||||
)
|
||||
|
||||
INSTALL(
|
||||
FILES
|
||||
PyGui/FemCommands.py
|
||||
@@ -107,6 +121,7 @@ INSTALL(
|
||||
PyGui/_TaskPanelFemMeshRegion.py
|
||||
PyGui/_TaskPanelFemResultShow.py
|
||||
PyGui/_TaskPanelFemSolverCalculix.py
|
||||
PyGui/_TaskPanelFemSolverControl.py
|
||||
PyGui/_ViewProviderFemConstraintSelfWeight.py
|
||||
PyGui/_ViewProviderFemElementFluid1D.py
|
||||
PyGui/_ViewProviderFemElementGeometry1D.py
|
||||
|
||||
@@ -30,6 +30,8 @@ __url__ = "http://www.freecadweb.org"
|
||||
from .FemCommands import FemCommands
|
||||
import FreeCADGui
|
||||
from PySide import QtCore, QtGui
|
||||
import femsolver.run
|
||||
import FemUtils
|
||||
|
||||
|
||||
class _CommandFemSolverRun(FemCommands):
|
||||
@@ -74,5 +76,35 @@ class _CommandFemSolverRun(FemCommands):
|
||||
else:
|
||||
QtGui.QMessageBox.critical(None, "Not known solver type", message)
|
||||
|
||||
def _newActivated(self):
|
||||
solver = self._getSelectedSolver()
|
||||
if solver is not None:
|
||||
try:
|
||||
machine = femsolver.run.getMachine(solver)
|
||||
except femsolver.run.MustSaveError:
|
||||
QtGui.QMessageBox.critical(
|
||||
FreeCADGui.getMainWindow(),
|
||||
"Can't start Solver",
|
||||
"Please save the file before executing the solver. "
|
||||
"This must be done because the location of the working "
|
||||
"directory is set to \"Beside .fcstd File\".")
|
||||
return
|
||||
except femsolver.run.DirectoryDoesNotExist:
|
||||
QtGui.QMessageBox.critical(
|
||||
FreeCADGui.getMainWindow(),
|
||||
"Can't start Solver",
|
||||
"Selected working directory doesn't exist.")
|
||||
return
|
||||
if not machine.running:
|
||||
machine.reset()
|
||||
machine.target = femsolver.run.RESULTS
|
||||
machine.start()
|
||||
|
||||
def _getSelectedSolver(self):
|
||||
sel = FreeCADGui.Selection.getSelection()
|
||||
if len(sel) == 1 and sel[0].isDerivedFrom("Fem::FemSolverObjectPython"):
|
||||
return sel[0]
|
||||
return None
|
||||
|
||||
|
||||
FreeCADGui.addCommand('FEM_SolverRun', _CommandFemSolverRun())
|
||||
|
||||
336
src/Mod/Fem/PyGui/_TaskPanelFemSolverControl.py
Normal file
336
src/Mod/Fem/PyGui/_TaskPanelFemSolverControl.py
Normal file
@@ -0,0 +1,336 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2017 - Markus Hovorka <m.hovorka@live.de> *
|
||||
# * *
|
||||
# * 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__ = "Solver Job Control Task Panel"
|
||||
__author__ = "Markus Hovorka"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
|
||||
|
||||
from PySide import QtCore
|
||||
from PySide import QtGui
|
||||
|
||||
import FreeCADGui as Gui
|
||||
import femsolver.run
|
||||
import femsolver.report
|
||||
|
||||
|
||||
_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)
|
||||
machineStoped = QtCore.Signal(object)
|
||||
machineStatusChanged = QtCore.Signal(str)
|
||||
machineStatusCleared = QtCore.Signal()
|
||||
machineTimeChanged = QtCore.Signal(float)
|
||||
machineStateChanged = QtCore.Signal(float)
|
||||
|
||||
def __init__(self, machine):
|
||||
super(ControlTaskPanel, self).__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 doesn't 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.machineStoped.connect(self._timer.stop)
|
||||
self.machineStoped.connect(self._displayReport)
|
||||
self.machineStoped.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 int(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.signalStoped.add(self._stopedProxy)
|
||||
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.signalStoped.remove(self._stopedProxy)
|
||||
self.machine.signalState.remove(self._stateProxy)
|
||||
|
||||
def _startedProxy(self):
|
||||
self.machineStarted.emit(self.machine)
|
||||
|
||||
def _stopedProxy(self):
|
||||
self.machineStoped.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(ControlWidget, self).__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)
|
||||
|
||||
# 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)
|
||||
0
src/Mod/Fem/femsolver/__init__.py
Normal file
0
src/Mod/Fem/femsolver/__init__.py
Normal file
94
src/Mod/Fem/femsolver/report.py
Normal file
94
src/Mod/Fem/femsolver/report.py
Normal file
@@ -0,0 +1,94 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2017 - Markus Hovorka <m.hovorka@live.de> *
|
||||
# * *
|
||||
# * 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__ = "report"
|
||||
__author__ = "Markus Hovorka"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
|
||||
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
INFO = 10
|
||||
WARNING = 20
|
||||
ERROR = 30
|
||||
|
||||
|
||||
def display(report, title=None, text=None):
|
||||
if App.GuiUp:
|
||||
displayGui(report, title, text)
|
||||
else:
|
||||
displayLog(report)
|
||||
|
||||
|
||||
def displayGui(report, title=None, text=None):
|
||||
import FreeCADGui as Gui
|
||||
from . import reportdialog
|
||||
if not report.isEmpty():
|
||||
mw = Gui.getMainWindow()
|
||||
dialog = reportdialog.ReportDialog(
|
||||
report, title, text, mw)
|
||||
dialog.exec_()
|
||||
|
||||
|
||||
def displayLog(report):
|
||||
for i in report.infos:
|
||||
App.Console.PrintLog("%s\n" % i)
|
||||
for w in report.warnings:
|
||||
App.Console.PrintWarning("%s\n" % w)
|
||||
for e in report.errors:
|
||||
App.Console.PrintError("%s\n" % e)
|
||||
|
||||
|
||||
class Report(object):
|
||||
|
||||
def __init__(self):
|
||||
self.infos = []
|
||||
self.warnings = []
|
||||
self.errors = []
|
||||
|
||||
def extend(self, report):
|
||||
self.infos.extend(report.infos)
|
||||
self.warnings.extend(report.warnings)
|
||||
self.errors.extend(report.errors)
|
||||
|
||||
def getLevel(self):
|
||||
if self.errors:
|
||||
return ERROR
|
||||
if self.warnings:
|
||||
return WARNING
|
||||
if self.infos:
|
||||
return INFO
|
||||
return None
|
||||
|
||||
def isEmpty(self):
|
||||
return not (self.infos or self.warnings or self.errors)
|
||||
|
||||
def info(self, msg):
|
||||
self.infos.append(msg)
|
||||
|
||||
def warning(self, msg):
|
||||
self.warnings.append(msg)
|
||||
|
||||
def error(self, msg):
|
||||
self.errors.append(msg)
|
||||
71
src/Mod/Fem/femsolver/reportdialog.py
Normal file
71
src/Mod/Fem/femsolver/reportdialog.py
Normal file
@@ -0,0 +1,71 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2017 - Markus Hovorka <m.hovorka@live.de> *
|
||||
# * *
|
||||
# * 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__ = "reportdialog"
|
||||
__author__ = "Markus Hovorka"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
|
||||
|
||||
from PySide import QtGui
|
||||
|
||||
|
||||
ERROR_COLOR = "red"
|
||||
WARNING_COLOR = "#ffaa00"
|
||||
INFO_COLOR = "blue"
|
||||
|
||||
|
||||
class ReportDialog(QtGui.QDialog):
|
||||
|
||||
def __init__(self, report, title="Report", text=None, parent=None):
|
||||
super(ReportDialog, self).__init__(parent)
|
||||
msgDetails = QtGui.QTextEdit()
|
||||
msgDetails.setReadOnly(True)
|
||||
msgDetails.setHtml(self._getText(report))
|
||||
bttBox = QtGui.QDialogButtonBox(QtGui.QDialogButtonBox.Ok)
|
||||
bttBox.accepted.connect(self.close)
|
||||
layout = QtGui.QVBoxLayout()
|
||||
if text is not None:
|
||||
textLbl = QtGui.QLabel(text)
|
||||
textLbl.setWordWrap(True)
|
||||
layout.addWidget(textLbl)
|
||||
layout.addWidget(msgDetails)
|
||||
layout.addWidget(bttBox)
|
||||
self.setWindowTitle(title)
|
||||
self.setLayout(layout)
|
||||
self.resize(300, 200)
|
||||
|
||||
def _getText(self, report):
|
||||
text = ""
|
||||
for i in report.infos:
|
||||
line = "<b>Info:</b> %s" % i
|
||||
text += "%s<br>" % self._getColoredLine(line, INFO_COLOR)
|
||||
for w in report.warnings:
|
||||
line = "<b>Warning:</b> %s" % w
|
||||
text += "%s<br>" % self._getColoredLine(line, WARNING_COLOR)
|
||||
for e in report.errors:
|
||||
line = "<b>Error:</b> %s" % e
|
||||
text += "%s<br>" % self._getColoredLine(line, ERROR_COLOR)
|
||||
return text
|
||||
|
||||
def _getColoredLine(self, text, color):
|
||||
return '<font color="%s">%s</font>' % (color, text)
|
||||
420
src/Mod/Fem/femsolver/run.py
Normal file
420
src/Mod/Fem/femsolver/run.py
Normal file
@@ -0,0 +1,420 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2017 - Markus Hovorka <m.hovorka@live.de> *
|
||||
# * *
|
||||
# * 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__ = "run"
|
||||
__author__ = "Markus Hovorka"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
|
||||
|
||||
import os
|
||||
import os.path
|
||||
import tempfile
|
||||
import threading
|
||||
import shutil
|
||||
|
||||
import FreeCAD as App
|
||||
import FemUtils
|
||||
from . import settings
|
||||
from . import signal
|
||||
from . import task
|
||||
|
||||
|
||||
CHECK = 0
|
||||
PREPARE = 1
|
||||
SOLVE = 2
|
||||
RESULTS = 3
|
||||
DONE = 4
|
||||
|
||||
|
||||
_machines = {}
|
||||
_dirTypes = {}
|
||||
|
||||
|
||||
def getMachine(solver, path=None):
|
||||
_DocObserver.attach()
|
||||
m = _machines.get(solver)
|
||||
if m is None or not _isPathValid(m, path):
|
||||
m = _createMachine(solver, path)
|
||||
return m
|
||||
|
||||
|
||||
def _isPathValid(m, path):
|
||||
t = _dirTypes[m.directory]
|
||||
setting = settings.getDirSetting()
|
||||
if path is not None:
|
||||
return t is None and m.directory == path
|
||||
if setting == settings.BESIDE:
|
||||
if t == settings.BESIDE:
|
||||
base = os.path.split(m.directory.rstrip("/"))[0]
|
||||
return base == _getBesideBase(m.solver)
|
||||
return False
|
||||
if setting == settings.TEMPORARY:
|
||||
return t == settings.TEMPORARY
|
||||
if setting == settings.CUSTOM:
|
||||
if t == settings.CUSTOM:
|
||||
firstBase = os.path.split(m.directory.rstrip("/"))[0]
|
||||
customBase = os.path.split(firstBase)[0]
|
||||
return customBase == _getCustomBase(m.solver)
|
||||
return False
|
||||
|
||||
|
||||
def _createMachine(solver, path):
|
||||
global _dirTypes
|
||||
setting = settings.getDirSetting()
|
||||
if path is not None:
|
||||
_dirTypes[path] = None
|
||||
elif setting == settings.BESIDE:
|
||||
path = _getBesideDir(solver)
|
||||
_dirTypes[path] = settings.BESIDE
|
||||
elif setting == settings.TEMPORARY:
|
||||
path = _getTempDir(solver)
|
||||
_dirTypes[path] = settings.TEMPORARY
|
||||
elif setting == settings.CUSTOM:
|
||||
path = _getCustomDir(solver)
|
||||
_dirTypes[path] = settings.CUSTOM
|
||||
m = solver.Proxy.createMachine(solver, path)
|
||||
oldMachine = _machines.get(solver)
|
||||
if oldMachine is not None:
|
||||
del _dirTypes[oldMachine.directory]
|
||||
_machines[solver] = m
|
||||
return m
|
||||
|
||||
|
||||
def _getTempDir(solver):
|
||||
return tempfile.mkdtemp(prefix="fem")
|
||||
|
||||
|
||||
def _getBesideDir(solver):
|
||||
base = _getBesideBase(solver)
|
||||
specificPath = os.path.join(base, solver.Label)
|
||||
specificPath = _getUniquePath(specificPath)
|
||||
if not os.path.isdir(specificPath):
|
||||
os.makedirs(specificPath)
|
||||
return specificPath
|
||||
|
||||
|
||||
def _getBesideBase(solver):
|
||||
fcstdPath = solver.Document.FileName
|
||||
if fcstdPath == "":
|
||||
raise MustSaveError()
|
||||
return os.path.splitext(fcstdPath)[0]
|
||||
|
||||
|
||||
def _getCustomDir(solver):
|
||||
base = _getCustomBase(solver)
|
||||
specificPath = os.path.join(
|
||||
base, solver.Document.Name, solver.Label)
|
||||
specificPath = _getUniquePath(specificPath)
|
||||
if not os.path.isdir(specificPath):
|
||||
os.makedirs(specificPath)
|
||||
return specificPath
|
||||
|
||||
|
||||
def _getCustomBase(solver):
|
||||
path = settings.getCustomDir()
|
||||
if not os.path.isdir(path):
|
||||
raise DirectoryDoesNotExist("Invalid path")
|
||||
return path
|
||||
|
||||
|
||||
def _getUniquePath(path):
|
||||
postfix = 1
|
||||
if path in _dirTypes:
|
||||
path += "_%03d" % postfix
|
||||
while path in _dirTypes:
|
||||
postfix += 1
|
||||
path = path[:-4] + "_%03d" % postfix
|
||||
return path
|
||||
|
||||
|
||||
class BaseTask(task.Thread):
|
||||
|
||||
def __init__(self):
|
||||
super(BaseTask, self).__init__()
|
||||
self.solver = None
|
||||
self.directory = None
|
||||
|
||||
@property
|
||||
def analysis(self):
|
||||
return FemUtils.findAnalysisOfMember(self.solver)
|
||||
|
||||
|
||||
class Machine(BaseTask):
|
||||
|
||||
def __init__(
|
||||
self, solver, directory, check,
|
||||
prepare, solve, results):
|
||||
super(Machine, self).__init__()
|
||||
self.solver = solver
|
||||
self.directory = directory
|
||||
self.signalState = set()
|
||||
self.check = check
|
||||
self.prepare = prepare
|
||||
self.solve = solve
|
||||
self.results = results
|
||||
self.target = RESULTS
|
||||
self._state = CHECK
|
||||
self._pendingState = None
|
||||
self._isReset = False
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
def run(self):
|
||||
self._confTasks()
|
||||
self._isReset = False
|
||||
self._pendingState = self.state
|
||||
while (not self.aborted and not self.failed
|
||||
and self._pendingState <= self.target):
|
||||
task = self._getTask(self._pendingState)
|
||||
self._runTask(task)
|
||||
self.report.extend(task.report)
|
||||
if task.failed:
|
||||
self.fail()
|
||||
elif task.aborted:
|
||||
self.abort()
|
||||
else:
|
||||
self._pendingState += 1
|
||||
self._applyPending()
|
||||
|
||||
def reset(self, newState=CHECK):
|
||||
state = (self.state
|
||||
if self._pendingState is None
|
||||
else self._pendingState)
|
||||
if newState < state:
|
||||
self._isReset = True
|
||||
self._state = newState
|
||||
signal.notify(self.signalState)
|
||||
|
||||
def _confTasks(self):
|
||||
tasks = [
|
||||
self.check,
|
||||
self.prepare,
|
||||
self.solve,
|
||||
self.results
|
||||
]
|
||||
for t in tasks:
|
||||
t.solver = self.solver
|
||||
t.directory = self.directory
|
||||
|
||||
def _applyPending(self):
|
||||
if not self._isReset:
|
||||
self._state = self._pendingState
|
||||
signal.notify(self.signalState)
|
||||
self._isReset = False
|
||||
self._pendingState = None
|
||||
|
||||
def _runTask(self, task):
|
||||
|
||||
def statusProxy(line):
|
||||
self.pushStatus(line)
|
||||
|
||||
def killer():
|
||||
task.abort()
|
||||
self.signalAbort.add(killer)
|
||||
task.signalStatus.add(statusProxy)
|
||||
task.start()
|
||||
task.join()
|
||||
self.signalAbort.remove(killer)
|
||||
task.signalStatus.remove(statusProxy)
|
||||
|
||||
def _getTask(self, state):
|
||||
if state == CHECK:
|
||||
return self.check
|
||||
elif state == PREPARE:
|
||||
return self.prepare
|
||||
elif state == SOLVE:
|
||||
return self.solve
|
||||
elif state == RESULTS:
|
||||
return self.results
|
||||
return None
|
||||
|
||||
|
||||
class Check(BaseTask):
|
||||
|
||||
def checkMesh(self):
|
||||
meshes = FemUtils.getMember(
|
||||
self.analysis, "Fem::FemMeshObject")
|
||||
if len(meshes) == 0:
|
||||
self.report.error("Missing a mesh object.")
|
||||
self.fail()
|
||||
return False
|
||||
elif len(meshes) > 1:
|
||||
self.report.error(
|
||||
"Too many meshes. "
|
||||
"More than one mesh is not supported.")
|
||||
self.fail()
|
||||
return False
|
||||
return True
|
||||
|
||||
def checkMaterial(self):
|
||||
matObjs = FemUtils.getMember(
|
||||
self.analysis, "App::MaterialObjectPython")
|
||||
if len(matObjs) == 0:
|
||||
self.report.error(
|
||||
"No material object found. "
|
||||
"At least one material is required.")
|
||||
self.fail()
|
||||
return False
|
||||
return True
|
||||
|
||||
def checkSupported(self, allSupported):
|
||||
for m in self.analysis.Member:
|
||||
if FemUtils.isOfType(m, "Fem::Constraint"):
|
||||
supported = False
|
||||
for sc in allSupported:
|
||||
if FemUtils.isOfType(m, *sc):
|
||||
supported = True
|
||||
if not supported:
|
||||
self.report.warning(
|
||||
"Ignored unsupported constraint: %s" % m.Label)
|
||||
return True
|
||||
|
||||
|
||||
class Solve(BaseTask):
|
||||
pass
|
||||
|
||||
|
||||
class Prepare(BaseTask):
|
||||
pass
|
||||
|
||||
|
||||
class Results(BaseTask):
|
||||
pass
|
||||
|
||||
|
||||
class _DocObserver(object):
|
||||
|
||||
_instance = None
|
||||
_WHITELIST = [
|
||||
"Fem::Constraint",
|
||||
"App::MaterialObject",
|
||||
"Fem::FemMeshObject",
|
||||
]
|
||||
_BLACKLIST_PROPS = [
|
||||
"Label",
|
||||
"ElmerOutput",
|
||||
"ElmerResult"
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
self._saved = {}
|
||||
for doc in App.listDocuments().itervalues():
|
||||
for obj in doc.Objects:
|
||||
if obj.isDerivedFrom("Fem::FemAnalysis"):
|
||||
self._saved[obj] = obj.Member
|
||||
|
||||
@classmethod
|
||||
def attach(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
App.addDocumentObserver(cls._instance)
|
||||
|
||||
def slotDeletedObject(self, obj):
|
||||
self._checkModel(obj)
|
||||
if obj in _machines:
|
||||
self._deleteMachine(obj)
|
||||
|
||||
def slotChangedObject(self, obj, prop):
|
||||
if prop not in self._BLACKLIST_PROPS:
|
||||
self._checkAnalysis(obj)
|
||||
self._checkEquation(obj)
|
||||
self._checkSolver(obj)
|
||||
self._checkModel(obj)
|
||||
|
||||
def slotDeletedDocument(self, doc):
|
||||
for obj in doc.Objects:
|
||||
if obj in _machines:
|
||||
self._deleteMachine(obj)
|
||||
|
||||
def _deleteMachine(self, obj):
|
||||
m = _machines[obj]
|
||||
t = _dirTypes[m.directory]
|
||||
|
||||
def delegate():
|
||||
m.join()
|
||||
if t == settings.TEMPORARY:
|
||||
shutil.rmtree(m.directory)
|
||||
del _dirTypes[m.directory]
|
||||
del _machines[obj]
|
||||
m.abort()
|
||||
thread = threading.Thread(target=delegate)
|
||||
thread.daemon = False
|
||||
thread.start()
|
||||
|
||||
def _checkEquation(self, obj):
|
||||
for o in obj.Document.Objects:
|
||||
if (FemUtils.isDerivedFrom(o, "Fem::FemSolverObject")
|
||||
and hasattr(o, "Group") and obj in o.Group):
|
||||
if o in _machines:
|
||||
_machines[o].reset()
|
||||
|
||||
def _checkSolver(self, obj):
|
||||
analysis = FemUtils.findAnalysisOfMember(obj)
|
||||
for m in _machines.itervalues():
|
||||
if analysis == m.analysis and obj == m.solver:
|
||||
m.reset()
|
||||
|
||||
def _checkAnalysis(self, obj):
|
||||
if FemUtils.isDerivedFrom(obj, "Fem::FemAnalysis"):
|
||||
deltaObjs = self._getAdded(obj)
|
||||
if deltaObjs:
|
||||
reset = False
|
||||
for o in deltaObjs:
|
||||
if self._partOfModel(o):
|
||||
reset = True
|
||||
if reset:
|
||||
self._resetAll(obj)
|
||||
|
||||
def _checkModel(self, obj):
|
||||
if self._partOfModel(obj):
|
||||
analysis = FemUtils.findAnalysisOfMember(obj)
|
||||
if analysis is not None:
|
||||
self._resetAll(analysis)
|
||||
|
||||
def _getAdded(self, analysis):
|
||||
if analysis not in self._saved:
|
||||
self._saved[analysis] = []
|
||||
delta = set(analysis.Member) - set(self._saved[analysis])
|
||||
self._saved[analysis] = analysis.Member
|
||||
return delta
|
||||
|
||||
def _resetAll(self, analysis):
|
||||
for m in _machines.itervalues():
|
||||
if analysis == m.analysis:
|
||||
m.reset()
|
||||
|
||||
def _partOfModel(self, obj):
|
||||
for t in self._WHITELIST:
|
||||
if FemUtils.isDerivedFrom(obj, t):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class MustSaveError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DirectoryDoesNotExist(Exception):
|
||||
pass
|
||||
89
src/Mod/Fem/femsolver/settings.py
Normal file
89
src/Mod/Fem/femsolver/settings.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2017 - Markus Hovorka <m.hovorka@live.de> *
|
||||
# * *
|
||||
# * 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__ = "settings"
|
||||
__author__ = "Markus Hovorka"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
|
||||
|
||||
import distutils.spawn
|
||||
import FreeCAD as App
|
||||
|
||||
|
||||
TEMPORARY = "temporary"
|
||||
BESIDE = "beside"
|
||||
CUSTOM = "custom"
|
||||
|
||||
_ELMER_PARAM = "User parameter:BaseApp/Preferences/Mod/Fem/Elmer"
|
||||
_GRID_PARAM = "User parameter:BaseApp/Preferences/Mod/Fem/Grid"
|
||||
|
||||
|
||||
class _BinaryDlg(object):
|
||||
|
||||
def __init__(self, default, param, useDefault, customPath):
|
||||
self.default = default
|
||||
self.param = param
|
||||
self.useDefault = useDefault
|
||||
self.customPath = customPath
|
||||
|
||||
def getBinary(self):
|
||||
paramObj = App.ParamGet(self.param)
|
||||
binary = self.default
|
||||
if not paramObj.GetBool(self.useDefault):
|
||||
binary = paramObj.GetString(self.customPath)
|
||||
return distutils.spawn.find_executable(binary)
|
||||
|
||||
|
||||
_BINARIES = {
|
||||
"ElmerSolver": _BinaryDlg(
|
||||
default="ElmerSolver",
|
||||
param=_ELMER_PARAM,
|
||||
useDefault="UseStandardElmerLocation",
|
||||
customPath="elmerBinaryPath"),
|
||||
"ElmerGrid": _BinaryDlg(
|
||||
default="ElmerGrid",
|
||||
param=_GRID_PARAM,
|
||||
useDefault="UseStandardGridLocation",
|
||||
customPath="gridBinaryPath"),
|
||||
}
|
||||
|
||||
|
||||
def getBinary(name):
|
||||
if name in _BINARIES:
|
||||
return _BINARIES[name].getBinary()
|
||||
return None
|
||||
|
||||
|
||||
def getCustomDir():
|
||||
param = App.ParamGet(_ELMER_PARAM)
|
||||
return param.GetString("CustomDirectoryPath")
|
||||
|
||||
|
||||
def getDirSetting():
|
||||
param = App.ParamGet(_ELMER_PARAM)
|
||||
if param.GetBool("UseTempDirectory"):
|
||||
return TEMPORARY
|
||||
elif param.GetBool("UseBesideDirectory"):
|
||||
return BESIDE
|
||||
elif param.GetBool("UseCustomDirectory"):
|
||||
return CUSTOM
|
||||
31
src/Mod/Fem/femsolver/signal.py
Normal file
31
src/Mod/Fem/femsolver/signal.py
Normal file
@@ -0,0 +1,31 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2017 - Markus Hovorka <m.hovorka@live.de> *
|
||||
# * *
|
||||
# * 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__ = "signal"
|
||||
__author__ = "Markus Hovorka"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
|
||||
|
||||
def notify(signal, *args):
|
||||
for slot in signal:
|
||||
slot(*args)
|
||||
108
src/Mod/Fem/femsolver/solverbase.py
Normal file
108
src/Mod/Fem/femsolver/solverbase.py
Normal file
@@ -0,0 +1,108 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2017 - Markus Hovorka <m.hovorka@live.de> *
|
||||
# * *
|
||||
# * 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__ = "General Solver Object"
|
||||
__author__ = "Markus Hovorka"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
|
||||
|
||||
from PySide import QtGui
|
||||
|
||||
import FreeCAD as App
|
||||
from . import run
|
||||
|
||||
if App.GuiUp:
|
||||
import FreeCADGui as Gui
|
||||
import PyGui._TaskPanelFemSolverControl
|
||||
|
||||
|
||||
class Proxy(object):
|
||||
|
||||
BaseType = "Fem::FemSolverObjectPython"
|
||||
|
||||
def __init__(self, obj):
|
||||
obj.Proxy = self
|
||||
obj.addExtension("App::GroupExtensionPython", self)
|
||||
|
||||
def createMachine(self, obj, directory):
|
||||
raise NotImplementedError()
|
||||
|
||||
def createEquation(self, obj, eqId):
|
||||
raise NotImplementedError()
|
||||
|
||||
def isSupported(self, equation):
|
||||
raise NotImplementedError()
|
||||
|
||||
def addEquation(self, obj, eqId):
|
||||
obj.addObject(self.createEquation(
|
||||
obj.Document, eqId))
|
||||
|
||||
def editSupported(self):
|
||||
return False
|
||||
|
||||
def edit(self, directory):
|
||||
raise NotImplementedError()
|
||||
|
||||
def execute(self, obj):
|
||||
return True
|
||||
|
||||
|
||||
class ViewProxy(object):
|
||||
"""Proxy for FemSolverElmers View Provider."""
|
||||
|
||||
def __init__(self, vobj):
|
||||
vobj.Proxy = self
|
||||
vobj.addExtension("Gui::ViewProviderGroupExtensionPython", self)
|
||||
|
||||
def setEdit(self, vobj, mode=0):
|
||||
try:
|
||||
machine = run.getMachine(vobj.Object)
|
||||
except run.MustSaveError:
|
||||
QtGui.QMessageBox.critical(
|
||||
Gui.getMainWindow(),
|
||||
"Can't open Task Panel",
|
||||
"Please save the file before opening the task panel. "
|
||||
"This must be done because the location of the working "
|
||||
"directory is set to \"Beside .fcstd File\".")
|
||||
return False
|
||||
except run.DirectoryDoesNotExist:
|
||||
QtGui.QMessageBox.critical(
|
||||
Gui.getMainWindow(),
|
||||
"Can't open Task Panel",
|
||||
"Selected working directory doesn't exist.")
|
||||
return False
|
||||
task = PyGui._TaskPanelFemSolverControl.ControlTaskPanel(machine)
|
||||
Gui.Control.showDialog(task)
|
||||
return True
|
||||
|
||||
def unsetEdit(self, vobj, mode=0):
|
||||
Gui.Control.closeDialog()
|
||||
|
||||
def doubleClicked(self, vobj):
|
||||
if Gui.Control.activeDialog():
|
||||
Gui.Control.closeDialog()
|
||||
Gui.ActiveDocument.setEdit(vobj.Object.Name)
|
||||
return True
|
||||
|
||||
def attach(self, vobj):
|
||||
pass
|
||||
146
src/Mod/Fem/femsolver/task.py
Normal file
146
src/Mod/Fem/femsolver/task.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# ***************************************************************************
|
||||
# * *
|
||||
# * Copyright (c) 2017 - Markus Hovorka <m.hovorka@live.de> *
|
||||
# * *
|
||||
# * 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__ = "task"
|
||||
__author__ = "Markus Hovorka"
|
||||
__url__ = "http://www.freecadweb.org"
|
||||
|
||||
|
||||
import threading
|
||||
import time
|
||||
|
||||
from . import report
|
||||
from . import signal
|
||||
|
||||
|
||||
class Task(object):
|
||||
|
||||
def __init__(self):
|
||||
self.report = None
|
||||
self.signalStarting = set()
|
||||
self.signalStarted = set()
|
||||
self.signalStoping = set()
|
||||
self.signalStoped = set()
|
||||
self.signalAbort = set()
|
||||
self.signalStatus = set()
|
||||
self.signalStatusCleared = set()
|
||||
self.startTime = None
|
||||
self.stopTime = None
|
||||
self.running = False
|
||||
self._aborted = False
|
||||
self._failed = False
|
||||
self._status = []
|
||||
|
||||
def stoping():
|
||||
self.stopTime = time.time()
|
||||
self.running = False
|
||||
self.signalStoping.add(stoping)
|
||||
|
||||
@property
|
||||
def time(self):
|
||||
if self.startTime is not None:
|
||||
endTime = (
|
||||
self.stopTime
|
||||
if self.stopTime is not None
|
||||
else time.time())
|
||||
return endTime - self.startTime
|
||||
return None
|
||||
|
||||
@property
|
||||
def failed(self):
|
||||
return self._failed
|
||||
|
||||
@property
|
||||
def aborted(self):
|
||||
return self._aborted
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return "".join(self._status)
|
||||
|
||||
def start(self):
|
||||
self.report = report.Report()
|
||||
self.clearStatus()
|
||||
self._aborted = False
|
||||
self._failed = False
|
||||
self.stopTime = None
|
||||
self.startTime = time.time()
|
||||
self.running = True
|
||||
signal.notify(self.signalStarting)
|
||||
signal.notify(self.signalStarted)
|
||||
|
||||
def run(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def join(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def abort(self):
|
||||
self._aborted = True
|
||||
signal.notify(self.signalAbort)
|
||||
|
||||
def fail(self):
|
||||
self._failed = True
|
||||
|
||||
def pushStatus(self, line):
|
||||
self._status.append(line)
|
||||
signal.notify(self.signalStatus, line)
|
||||
|
||||
def clearStatus(self):
|
||||
self._status = []
|
||||
signal.notify(self.signalStatusCleared)
|
||||
|
||||
def protector(self):
|
||||
try:
|
||||
self.run()
|
||||
except:
|
||||
self.fail()
|
||||
raise
|
||||
|
||||
|
||||
class Thread(Task):
|
||||
|
||||
def __init__(self):
|
||||
super(Thread, self).__init__()
|
||||
self._thread = None
|
||||
|
||||
def start(self):
|
||||
super(Thread, self).start()
|
||||
self._thread = threading.Thread(
|
||||
target=self.protector)
|
||||
self._thread.daemon = True
|
||||
self._thread.start()
|
||||
self._attachObserver()
|
||||
|
||||
def join(self):
|
||||
if self._thread is not None:
|
||||
self._thread.join()
|
||||
|
||||
def _attachObserver(self):
|
||||
def waitForStop():
|
||||
self._thread.join()
|
||||
signal.notify(self.signalStoping)
|
||||
signal.notify(self.signalStoped)
|
||||
thread = threading.Thread(target=waitForStop)
|
||||
thread.daemon = True
|
||||
thread.start()
|
||||
Reference in New Issue
Block a user