diff --git a/src/Mod/Fem/App/CMakeLists.txt b/src/Mod/Fem/App/CMakeLists.txt index 3168d50ee7..66230eaa84 100644 --- a/src/Mod/Fem/App/CMakeLists.txt +++ b/src/Mod/Fem/App/CMakeLists.txt @@ -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) diff --git a/src/Mod/Fem/CMakeLists.txt b/src/Mod/Fem/CMakeLists.txt index e29a407724..e3ed9ec5cd 100755 --- a/src/Mod/Fem/CMakeLists.txt +++ b/src/Mod/Fem/CMakeLists.txt @@ -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 diff --git a/src/Mod/Fem/PyGui/_CommandFemSolverRun.py b/src/Mod/Fem/PyGui/_CommandFemSolverRun.py index cce1f8b1d2..49e1171d2f 100644 --- a/src/Mod/Fem/PyGui/_CommandFemSolverRun.py +++ b/src/Mod/Fem/PyGui/_CommandFemSolverRun.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()) diff --git a/src/Mod/Fem/PyGui/_TaskPanelFemSolverControl.py b/src/Mod/Fem/PyGui/_TaskPanelFemSolverControl.py new file mode 100644 index 0000000000..dba34d9adb --- /dev/null +++ b/src/Mod/Fem/PyGui/_TaskPanelFemSolverControl.py @@ -0,0 +1,336 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2017 - Markus Hovorka * +# * * +# * 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 = "%05.1f" % 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) diff --git a/src/Mod/Fem/femsolver/__init__.py b/src/Mod/Fem/femsolver/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Mod/Fem/femsolver/report.py b/src/Mod/Fem/femsolver/report.py new file mode 100644 index 0000000000..603c1ed0be --- /dev/null +++ b/src/Mod/Fem/femsolver/report.py @@ -0,0 +1,94 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2017 - Markus Hovorka * +# * * +# * 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) diff --git a/src/Mod/Fem/femsolver/reportdialog.py b/src/Mod/Fem/femsolver/reportdialog.py new file mode 100644 index 0000000000..a049beaf90 --- /dev/null +++ b/src/Mod/Fem/femsolver/reportdialog.py @@ -0,0 +1,71 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2017 - Markus Hovorka * +# * * +# * 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 = "Info: %s" % i + text += "%s
" % self._getColoredLine(line, INFO_COLOR) + for w in report.warnings: + line = "Warning: %s" % w + text += "%s
" % self._getColoredLine(line, WARNING_COLOR) + for e in report.errors: + line = "Error: %s" % e + text += "%s
" % self._getColoredLine(line, ERROR_COLOR) + return text + + def _getColoredLine(self, text, color): + return '%s' % (color, text) diff --git a/src/Mod/Fem/femsolver/run.py b/src/Mod/Fem/femsolver/run.py new file mode 100644 index 0000000000..8064e0d154 --- /dev/null +++ b/src/Mod/Fem/femsolver/run.py @@ -0,0 +1,420 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2017 - Markus Hovorka * +# * * +# * 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 diff --git a/src/Mod/Fem/femsolver/settings.py b/src/Mod/Fem/femsolver/settings.py new file mode 100644 index 0000000000..25f302bfe6 --- /dev/null +++ b/src/Mod/Fem/femsolver/settings.py @@ -0,0 +1,89 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2017 - Markus Hovorka * +# * * +# * 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 diff --git a/src/Mod/Fem/femsolver/signal.py b/src/Mod/Fem/femsolver/signal.py new file mode 100644 index 0000000000..87a30fea50 --- /dev/null +++ b/src/Mod/Fem/femsolver/signal.py @@ -0,0 +1,31 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2017 - Markus Hovorka * +# * * +# * 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) diff --git a/src/Mod/Fem/femsolver/solverbase.py b/src/Mod/Fem/femsolver/solverbase.py new file mode 100644 index 0000000000..f88603054d --- /dev/null +++ b/src/Mod/Fem/femsolver/solverbase.py @@ -0,0 +1,108 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2017 - Markus Hovorka * +# * * +# * 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 diff --git a/src/Mod/Fem/femsolver/task.py b/src/Mod/Fem/femsolver/task.py new file mode 100644 index 0000000000..f59adf7e8b --- /dev/null +++ b/src/Mod/Fem/femsolver/task.py @@ -0,0 +1,146 @@ +# *************************************************************************** +# * * +# * Copyright (c) 2017 - Markus Hovorka * +# * * +# * 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()