Merge pull request #14417 from marioalexis84/fem-constraint_body_heat_source

Fem: Add body heat source to CalculiX writer - fixes #11650
This commit is contained in:
Chris Hennes
2024-06-10 10:38:26 -05:00
committed by GitHub
12 changed files with 382 additions and 91 deletions
+1 -1
View File
@@ -551,7 +551,7 @@ UnitsSchemaInternal::schemaTranslate(const Quantity& quant, double& factor, QStr
}
}
else if (unit == Unit::DissipationRate) {
unitString = QString::fromLatin1("m^2/s^3");
unitString = QString::fromLatin1("W/kg");
factor = 1e6;
}
else if (unit == Unit::InverseLength) {
+1 -1
View File
@@ -538,7 +538,7 @@ QString UnitsSchemaMKS::schemaTranslate(const Quantity& quant, double& factor, Q
}
}
else if (unit == Unit::DissipationRate) {
unitString = QString::fromLatin1("m^2/s^3");
unitString = QString::fromLatin1("W/kg");
factor = 1e6;
}
else if (unit == Unit::InverseLength) {
+1
View File
@@ -221,6 +221,7 @@ SET(FemSolverCalculix_SRCS
femsolver/calculix/__init__.py
femsolver/calculix/solver.py
femsolver/calculix/tasks.py
femsolver/calculix/write_constraint_bodyheatsource.py
femsolver/calculix/write_constraint_centrif.py
femsolver/calculix/write_constraint_contact.py
femsolver/calculix/write_constraint_displacement.py
+105 -46
View File
@@ -13,56 +13,115 @@
<property name="windowTitle">
<string>Analysis feature properties</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<layout class="QVBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="pressureLbl">
<property name="text">
<string>Body heat in W/kg:</string>
<widget class="QGroupBox" name="gpb_heat_source">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
<property name="toolTip">
<string/>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>130</width>
<height>19</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="Gui::QuantitySpinBox" name="bodyheatQSB">
<property name="enabled">
<bool>true</bool>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>20</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="keyboardTracking">
<bool>true</bool>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>1000000000000000000000.000000000000000</double>
</property>
<property name="singleStep">
<double>50.000000000000000</double>
</property>
<property name="value">
<double>0.000000000000000</double>
<property name="title">
<string>Heat Source</string>
</property>
<layout class="QGridLayout" name="g_layout_heat_source">
<item row="0" column="0">
<widget class="QLabel" name="lbl_dissipation_rate">
<property name="text">
<string>Mode:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="cb_mode"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="lbl_total_power">
<property name="text">
<string>Total Power:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="Gui::QuantitySpinBox" name="qsb_total_power">
<property name="enabled">
<bool>true</bool>
</property>
<property name="unit" stdset="0">
<string notr="true">W</string>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>20</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="keyboardTracking">
<bool>true</bool>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>1000000000000000000000.000000000000000</double>
</property>
<property name="singleStep">
<double>50.000000000000000</double>
</property>
<property name="value">
<double>0.000000000000000</double>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="lbl_dissipation_rate">
<property name="text">
<string>Dissipation Rate:</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="Gui::QuantitySpinBox" name="qsb_dissipation_rate">
<property name="enabled">
<bool>true</bool>
</property>
<property name="unit" stdset="0">
<string notr="true">W/kg</string>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>20</height>
</size>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="keyboardTracking">
<bool>true</bool>
</property>
<property name="minimum">
<double>0.000000000000000</double>
</property>
<property name="maximum">
<double>1000000000000000000000.000000000000000</double>
</property>
<property name="singleStep">
<double>50.000000000000000</double>
</property>
<property name="value">
<double>0.000000000000000</double>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
+13
View File
@@ -135,6 +135,7 @@ class MeshSetsGetter():
# constraints element sets getter
self.get_constraints_centrif_elements()
self.get_constraints_bodyheatsource_elements()
# constraints node sets getter
self.get_constraints_fixed_nodes()
@@ -540,6 +541,18 @@ class MeshSetsGetter():
else:
self.get_solid_element_sets(self.member.cons_centrif)
def get_constraints_bodyheatsource_elements(self):
# get element ids and write them into the femobj
if not self.member.cons_bodyheatsource:
return
if (
len(self.member.cons_bodyheatsource) == 1
and not self.member.cons_bodyheatsource[0]["Object"].References
):
self.member.cons_bodyheatsource[0]["FEMElements"] = self.ccx_evolumes
else:
self.get_solid_element_sets(self.member.cons_bodyheatsource)
# ********************************************************************************************
# ********************************************************************************************
# element sets material and element geometry
+2
View File
@@ -784,6 +784,8 @@ def get_elset_short_name(
return "S" + str(i)
elif is_of_type(obj, "Fem::ConstraintCentrif"):
return "C" + str(i)
elif is_of_type(obj, "Fem::ConstraintBodyHeatSource"):
return "H" + str(i)
else:
FreeCAD.Console.PrintError(
"Error in creating short elset name "
@@ -1,6 +1,7 @@
# ***************************************************************************
# * Copyright (c) 2017 Markus Hovorka <[email protected]> *
# * Copyright (c) 2020 Bernd Hahnebach <[email protected]> *
# * Copyright (c) 2024 Mario Passaglia <[email protected]> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
@@ -23,15 +24,19 @@
# ***************************************************************************
__title__ = "FreeCAD FEM constraint body heat source document object"
__author__ = "Markus Hovorka, Bernd Hahnebach"
__author__ = "Markus Hovorka, Bernd Hahnebach, Mario Passaglia"
__url__ = "https://www.freecad.org"
## @package constraint_bodyheatsource
# \ingroup FEM
# \brief constraint body heat source object
import FreeCAD
from . import base_fempythonobject
_PropHelper = base_fempythonobject._PropHelper
class ConstraintBodyHeatSource(base_fempythonobject.BaseFemPythonObject):
@@ -39,18 +44,56 @@ class ConstraintBodyHeatSource(base_fempythonobject.BaseFemPythonObject):
def __init__(self, obj):
super(ConstraintBodyHeatSource, self).__init__(obj)
self.add_properties(obj)
for prop in self._get_properties():
prop.add_to_object(obj)
def _get_properties(self):
prop = []
prop.append(_PropHelper(
type = "App::PropertyDissipationRate",
name = "DissipationRate",
group = "Constraint Body Heat Source",
doc = "Power dissipated per unit mass",
value = "0 W/kg"
)
)
prop.append(_PropHelper(
type = "App::PropertyPower",
name = "TotalPower",
group = "Constraint Body Heat Source",
doc = "Total power dissipated",
value = "0 W"
)
)
prop.append(_PropHelper(
type = "App::PropertyEnumeration",
name = "Mode",
group = "Constraint Body Heat Source",
doc = "Switch quantity input mode",
value = ["Dissipation Rate", "Total Power"]
)
)
return prop
def onDocumentRestored(self, obj):
self.add_properties(obj)
# update old project with new properties
for prop in self._get_properties():
try:
obj.getPropertyByName(prop.name)
except:
prop.add_to_object(obj)
def add_properties(self, obj):
if not hasattr(obj, "HeatSource"):
obj.addProperty(
"App::PropertyFloat",
"HeatSource",
"Base",
"Body heat source"
)
obj.setPropertyStatus("HeatSource", "LockDynamic")
obj.HeatSource = 0.0
# migrate old HeatSource property
try:
value = obj.getPropertyByName("HeatSource")
obj.DissipationRate = FreeCAD.Units.Quantity(value, "W/kg")
obj.Mode = "Dissipation Rate"
obj.setPropertyStatus("HeatSource", "-LockDynamic")
obj.removeProperty("HeatSource")
except:
pass
@@ -0,0 +1,103 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
#/***************************************************************************
# * Copyright (c) 2024 Mario Passaglia <mpassaglia[at]cbc.uba.ar> *
# * *
# * This file is part of FreeCAD. *
# * *
# * FreeCAD is free software: you can redistribute it and/or modify it *
# * under the terms of the GNU Lesser General Public License as *
# * published by the Free Software Foundation, either version 2.1 of the *
# * License, or (at your option) any later version. *
# * *
# * FreeCAD 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 *
# * Lesser General Public License for more details. *
# * *
# * You should have received a copy of the GNU Lesser General Public *
# * License along with FreeCAD. If not, see *
# * <https://www.gnu.org/licenses/>. *
# * *
# **************************************************************************
__title__ = "FreeCAD FEM calculix constraint body heat source"
__author__ = "Mario Passaglia"
__url__ = "https://www.freecad.org"
import FreeCAD
import itertools
def get_analysis_types():
return ["thermomech"]
def get_sets_name():
return "constraints_bodyheatsource_element_sets"
def get_constraint_title():
return "Body Heat Source Constraints"
def get_before_write_meshdata_constraint():
return ""
def get_after_write_meshdata_constraint():
return ""
def get_before_write_constraint():
return ""
def get_after_write_constraint():
return ""
def write_meshdata_constraint(f, femobj, bodyheatsource_obj, ccxwriter):
f.write("*ELSET,ELSET={}\n".format(bodyheatsource_obj.Name))
if isinstance(femobj["FEMElements"], str):
f.write("{}\n".format(femobj["FEMElements"]))
else:
for e in femobj["FEMElements"]:
f.write("{},\n".format(e))
def write_constraint(f, femobj, bodyheatsource_obj, ccxwriter):
# floats read from ccx should use {:.13G}, see comment in writer module
# search referenced material
ref = bodyheatsource_obj.References[0]
ref_feat = ref[0]
ref_sub_obj = ref[1][0]
density = None
for mat in ccxwriter.member.mats_linear:
mat_ref = [*itertools.chain(*[itertools.product([i[0]],i[1]) for i in mat["Object"].References])]
if (ref_feat, ref_sub_obj) in mat_ref:
density = FreeCAD.Units.Quantity(mat["Object"].Material["Density"])
break
if not density:
# search material without references
for mat in ccxwriter.member.mats_linear:
if not mat["Object"].References:
density = FreeCAD.Units.Quantity(mat["Object"].Material["Density"])
break
# get data from the bodyheatsource_obj (DissipationRate is in power per unit mass)
if bodyheatsource_obj.Mode == "Dissipation Rate":
heat = bodyheatsource_obj.DissipationRate * density
elif bodyheatsource_obj.Mode == "Total Power":
volume = ref_feat.getSubObject(ref_sub_obj).Volume
heat = bodyheatsource_obj.TotalPower / FreeCAD.Units.Quantity(volume, "mm^3")
# write to file
f.write("*DFLUX\n")
f.write(
"{},BF,{:.13G}\n".format(
bodyheatsource_obj.Name, heat.getValueAs("t/(mm*s^3)").Value
)
)
f.write("\n")
+3
View File
@@ -36,6 +36,7 @@ import FreeCAD
from FreeCAD import Units
from . import write_constraint_centrif as con_centrif
from . import write_constraint_bodyheatsource as con_bodyheatsource
from . import write_constraint_contact as con_contact
from . import write_constraint_displacement as con_displacement
from . import write_constraint_fixed as con_fixed
@@ -160,6 +161,7 @@ class FemInputWriterCcx(writerbase.FemInputWriter):
# element sets constraints
self.write_constraints_meshsets(inpfile, self.member.cons_centrif, con_centrif)
self.write_constraints_meshsets(inpfile, self.member.cons_bodyheatsource, con_bodyheatsource)
# node sets
self.write_constraints_meshsets(inpfile, self.member.cons_fixed, con_fixed)
@@ -196,6 +198,7 @@ class FemInputWriterCcx(writerbase.FemInputWriter):
self.write_constraints_propdata(inpfile, self.member.cons_sectionprint, con_sectionprint)
self.write_constraints_propdata(inpfile, self.member.cons_selfweight, con_selfweight)
self.write_constraints_propdata(inpfile, self.member.cons_centrif, con_centrif)
self.write_constraints_propdata(inpfile, self.member.cons_bodyheatsource, con_bodyheatsource)
self.write_constraints_meshsets(inpfile, self.member.cons_force, con_force)
self.write_constraints_meshsets(inpfile, self.member.cons_pressure, con_pressure)
self.write_constraints_propdata(inpfile, self.member.cons_temperature, con_temperature)
@@ -30,6 +30,9 @@ __url__ = "https://www.freecad.org"
## \addtogroup FEM
# @{
import itertools
import FreeCAD
from .. import sifio
from .. import writer as general_writer
from femtools import membertools
@@ -138,7 +141,29 @@ class Heatwriter:
self.write.handled(tempObj)
def _outputHeatBodyForce(self, obj, name):
heatSource = self.write.getFromUi(obj.HeatSource, "W/kg", "L^2*T^-3")
if obj.Mode == "Dissipation Rate":
heatSource = obj.DissipationRate.getValueAs("W/kg").Value
elif obj.Mode == "Total Power":
ref = obj.References[0]
ref_feat = ref[0]
ref_sub_obj = ref[1][0]
density = None
for mat in self.write.getMember("App::MaterialObject"):
mat_ref = [*itertools.chain(*[itertools.product([i[0]],i[1]) for i in mat.References])]
if (ref_feat, ref_sub_obj) in mat_ref:
density = FreeCAD.Units.Quantity(mat.Material["Density"])
break
if not density:
# search material without references
for mat in self.write.getMember("App::MaterialObject"):
if not mat.References:
density = FreeCAD.Units.Quantity(mat.Material["Density"])
break
volume = ref_feat.getSubObject(ref_sub_obj).Volume
heatSource = (obj.TotalPower / (density*FreeCAD.Units.Quantity(volume, "mm^3"))).getValueAs("W/kg").Value
if heatSource == 0.0:
# a zero heat would break Elmer (division by zero)
raise general_writer.WriteError("The body heat source must not be zero!")
@@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org"
# \ingroup FEM
# \brief task panel for constraint bodyheatsource object
from PySide import QtCore
import FreeCAD
import FreeCADGui
@@ -41,15 +43,35 @@ from femtools import membertools
class _TaskPanel(object):
def __init__(self, obj):
self._obj = obj
self.obj = obj
self._paramWidget = FreeCADGui.PySideUic.loadUi(
self.parameter_widget = FreeCADGui.PySideUic.loadUi(
FreeCAD.getHomePath() + "Mod/Fem/Resources/ui/BodyHeatSource.ui")
self._initParamWidget()
self.init_parameter_widget()
QtCore.QObject.connect(
self.parameter_widget.qsb_dissipation_rate,
QtCore.SIGNAL("valueChanged(Base::Quantity)"),
self.dissipation_rate_changed
)
QtCore.QObject.connect(
self.parameter_widget.qsb_total_power,
QtCore.SIGNAL("valueChanged(Base::Quantity)"),
self.total_power_changed
)
QtCore.QObject.connect(
self.parameter_widget.cb_mode,
QtCore.SIGNAL("currentIndexChanged(int)"),
self.mode_changed
)
# geometry selection widget
# start with Solid in list!
self._selectionWidget = selection_widgets.GeometryElementsSelection(
self.selection_widget = selection_widgets.GeometryElementsSelection(
obj.References,
["Solid", "Face"],
True,
@@ -57,7 +79,7 @@ class _TaskPanel(object):
)
# form made from param and selection widget
self.form = [self._paramWidget, self._selectionWidget]
self.form = [self.selection_widget, self.parameter_widget]
analysis = obj.getParentGroup()
self._mesh = None
@@ -77,20 +99,22 @@ class _TaskPanel(object):
self._part.ViewObject.show()
def reject(self):
self._restoreVisibility()
self.restore_visibility()
FreeCADGui.ActiveDocument.resetEdit()
return True
def accept(self):
if self._obj.References != self._selectionWidget.references:
self._obj.References = self._selectionWidget.references
self._applyWidgetChanges()
self._obj.Document.recompute()
self.obj.References = self.selection_widget.references
self.obj.DissipationRate = self.dissipation_rate
self.obj.TotalPower = self.total_power
self.obj.Mode = self.mode
self.obj.Document.recompute()
FreeCADGui.ActiveDocument.resetEdit()
self._restoreVisibility()
self.restore_visibility()
return True
def _restoreVisibility(self):
def restore_visibility(self):
if self._mesh is not None and self._part is not None:
if self._meshVisible:
self._mesh.ViewObject.show()
@@ -101,21 +125,36 @@ class _TaskPanel(object):
else:
self._part.ViewObject.hide()
def _initParamWidget(self):
self._paramWidget.bodyheatQSB.setProperty(
'value', self._obj.HeatSource)
self._paramWidget.bodyheatQSB.setProperty("unit", "W/kg")
FreeCADGui.ExpressionBinding(self._paramWidget.bodyheatQSB).bind(self._obj, "HeatSource")
def _applyWidgetChanges(self):
bodyheat = None
try:
bodyheat = self._paramWidget.bodyheatQSB.property('value').getValueAs("W/kg")
except ValueError:
FreeCAD.Console.PrintMessage(
"Wrong input. Not recognised input: '{}' "
"Body heat has not been set.\n"
.format(self._paramWidget.bodyheatQSB.text())
)
if bodyheat is not None:
self._obj.HeatSource = float(bodyheat)
def init_parameter_widget(self):
self.dissipation_rate = self.obj.DissipationRate
self.total_power = self.obj.TotalPower
FreeCADGui.ExpressionBinding(self.parameter_widget.qsb_dissipation_rate)\
.bind(self.obj, "DissipationRate")
self.parameter_widget.qsb_dissipation_rate.setProperty("value", self.dissipation_rate)
FreeCADGui.ExpressionBinding(self.parameter_widget.qsb_total_power)\
.bind(self.obj, "TotalPower")
self.parameter_widget.qsb_total_power.setProperty("value", self.total_power)
self.mode = self.obj.Mode
self.mode_enum = self.obj.getEnumerationsOfProperty("Mode")
self.parameter_widget.cb_mode.addItems(self.mode_enum)
index = self.mode_enum.index(self.mode)
self.parameter_widget.cb_mode.setCurrentIndex(index)
self.mode_changed(index)
def dissipation_rate_changed(self, base_quantity_value):
self.dissipation_rate = base_quantity_value
def total_power_changed(self, base_quantity_value):
self.total_power = base_quantity_value
def mode_changed(self, index):
self.mode = self.mode_enum[index]
if self.mode == "Dissipation Rate":
self.parameter_widget.qsb_dissipation_rate.setEnabled(True)
self.parameter_widget.qsb_total_power.setEnabled(False)
elif self.mode == "Total Power":
self.parameter_widget.qsb_dissipation_rate.setEnabled(False)
self.parameter_widget.qsb_total_power.setEnabled(True)
+3
View File
@@ -289,6 +289,9 @@ class AnalysisMember():
self.cons_centrif = self.get_several_member(
"Fem::ConstraintCentrif"
)
self.cons_bodyheatsource = self.get_several_member(
"Fem::ConstraintBodyHeatSource"
)
self.cons_contact = self.get_several_member(
"Fem::ConstraintContact"
)