FEM: new solver framework, initial commit

This commit is contained in:
Markus Hovorka
2017-12-01 19:42:28 +01:00
committed by wmayer
parent b1becd5df3
commit 6f2dea1560
12 changed files with 1356 additions and 1 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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())

View 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)

View File

View 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)

View 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)

View 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

View 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

View 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)

View 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

View 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()