FEM: Add table post data visualization
This commit is contained in:
@@ -21,42 +21,70 @@
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
__title__ = "FreeCAD FEM postprocessing data exxtractor base objcts"
|
||||
__title__ = "FreeCAD FEM postprocessing data visualization base object"
|
||||
__author__ = "Stefan Tröger"
|
||||
__url__ = "https://www.freecad.org"
|
||||
|
||||
## @package base_fempostextractors
|
||||
# \ingroup FEM
|
||||
# \brief base objects for data extractors
|
||||
# \brief base objects for data visualizations
|
||||
|
||||
from vtkmodules.vtkCommonDataModel import vtkTable
|
||||
from vtkmodules.vtkCommonCore import vtkDoubleArray
|
||||
|
||||
from . import base_fempythonobject
|
||||
from . import base_fempostextractors
|
||||
|
||||
# helper functions
|
||||
# ################
|
||||
|
||||
def is_visualization_object(obj):
|
||||
if not obj:
|
||||
return False
|
||||
|
||||
if not hasattr(obj, "Proxy"):
|
||||
return False
|
||||
|
||||
return hasattr(obj.Proxy, "VisualizationType")
|
||||
|
||||
|
||||
def get_visualization_type(obj):
|
||||
# returns the extractor type string, or throws exception if
|
||||
# not a extractor
|
||||
return obj.Proxy.VisualizationType
|
||||
|
||||
|
||||
def is_visualization_extractor_type(obj, vistype):
|
||||
|
||||
# must be extractor
|
||||
if not base_fempostextractors.is_extractor_object(obj):
|
||||
return False
|
||||
|
||||
# must be visualization object
|
||||
if not is_visualization_object(obj):
|
||||
return False
|
||||
|
||||
# must be correct type
|
||||
if get_visualization_type(obj) != vistype:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
||||
# Base class for all visualizations
|
||||
# It collects all data from its extraction objects into a table.
|
||||
# Note: Never use directly, always subclass! This class does not create a
|
||||
# Visualization variable, hence will not work correctly.
|
||||
class PostVisualization(base_fempythonobject.BaseFemPythonObject):
|
||||
|
||||
|
||||
def __init__(self, obj):
|
||||
super().__init__(obj)
|
||||
obj.addExtension("App::GroupExtensionPython")
|
||||
self._setup_properties(obj)
|
||||
|
||||
|
||||
def _setup_properties(self, obj):
|
||||
pl = obj.PropertiesList
|
||||
for prop in self._get_properties():
|
||||
@@ -64,8 +92,96 @@ class PostVisualization(base_fempythonobject.BaseFemPythonObject):
|
||||
prop.add_to_object(obj)
|
||||
|
||||
|
||||
def _get_properties(self):
|
||||
# override if subclass wants to add additional properties
|
||||
|
||||
prop = [
|
||||
base_fempostextractors._PropHelper(
|
||||
type="Fem::PropertyPostDataObject",
|
||||
name="Table",
|
||||
group="Base",
|
||||
doc="The data table that stores the data for visualization",
|
||||
value=vtkTable(),
|
||||
),
|
||||
]
|
||||
return prop
|
||||
|
||||
|
||||
def onDocumentRestored(self, obj):
|
||||
# if a new property was added we handle it by setup
|
||||
# Override if subclass needs to handle changed property type
|
||||
|
||||
self._setup_properties(obj)
|
||||
|
||||
def _get_properties(self):
|
||||
return []
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
# Ensure only correct child object types are in the group
|
||||
|
||||
if prop == "Group":
|
||||
# check if all objects are allowed
|
||||
|
||||
children = obj.Group
|
||||
for child in obj.Group:
|
||||
if not is_visualization_extractor_type(child, self.VisualizationType):
|
||||
FreeCAD.Console.PrintWarning(f"{child.Label} is not a {self.VisualizationType} extraction object, cannot be added")
|
||||
children.remove(child)
|
||||
|
||||
if len(obj.Group) != len(children):
|
||||
obj.Group = children
|
||||
|
||||
|
||||
def execute(self, obj):
|
||||
# Collect all extractor child data into our table
|
||||
# Note: Each childs table can have different number of rows. We need
|
||||
# to pad the date for our table in this case
|
||||
|
||||
rows = self.getLongestColumnLength(obj)
|
||||
table = vtkTable()
|
||||
for child in obj.Group:
|
||||
|
||||
# If child has no Source, its table should be empty. However,
|
||||
# it would theoretical be possible that child source was set
|
||||
# to none without recompute, and the visualization was manually
|
||||
# recomputed afterwards
|
||||
if not child.Source and (c_table.GetNumberOfColumns() > 0):
|
||||
FreeCAD.Console.PrintWarning(f"{child.Label} has data, but no Source object. Will be ignored")
|
||||
continue
|
||||
|
||||
c_table = child.Table
|
||||
for i in range(c_table.GetNumberOfColumns()):
|
||||
c_array = c_table.GetColumn(i)
|
||||
array = vtkDoubleArray()
|
||||
|
||||
if c_array.GetNumberOfTuples() == rows:
|
||||
# simple deep copy is enough
|
||||
array.DeepCopy(c_array)
|
||||
|
||||
else:
|
||||
array.SetNumberOfComponents(c_array.GetNumberOfComponents())
|
||||
array.SetNumberOfTuples(rows)
|
||||
array.Fill(0) # so that all non-used entries are set to 0
|
||||
for i in range(c_array.GetNumberOfTuples()):
|
||||
array.SetTuple(i, c_array.GetTuple(i))
|
||||
|
||||
array.SetName(f"{child.Source.Name}: {c_array.GetName()}")
|
||||
table.AddColumn(array)
|
||||
|
||||
|
||||
obj.Table = table
|
||||
return False
|
||||
|
||||
|
||||
def getLongestColumnLength(self, obj):
|
||||
# iterate all extractor children and get the column lengths
|
||||
|
||||
length = 0
|
||||
for child in obj.Group:
|
||||
if base_fempostextractors.is_extractor_object(child):
|
||||
table = child.Table
|
||||
if table.GetNumberOfColumns() > 0:
|
||||
# we assume all columns of an extractor have same length
|
||||
num = table.GetColumn(0).GetNumberOfTuples()
|
||||
if num > length:
|
||||
length = num
|
||||
|
||||
return length
|
||||
|
||||
@@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org"
|
||||
# \ingroup FEM
|
||||
# \brief Post processing plot displaying lines
|
||||
|
||||
import FreeCAD
|
||||
|
||||
from . import base_fempostextractors
|
||||
from . import base_fempythonobject
|
||||
_PropHelper = base_fempythonobject._PropHelper
|
||||
@@ -176,9 +178,9 @@ class PostIndexOverFrames1D(base_fempostextractors.Extractor1D):
|
||||
frame_array.SetTuple(i, idx, array)
|
||||
|
||||
if frame_array.GetNumberOfComponents() > 1:
|
||||
frame_array.SetName(f"{obj.XField} ({obj.XComponent})")
|
||||
frame_array.SetName(f"{obj.XField} ({obj.XComponent}) @Idx {obj.Index}")
|
||||
else:
|
||||
frame_array.SetName(f"{obj.XField}")
|
||||
frame_array.SetName(f"{obj.XField} @Idx {obj.Index}")
|
||||
|
||||
self._x_array_component_to_table(obj, frame_array, table)
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ __url__ = "https://www.freecad.org"
|
||||
# \ingroup FEM
|
||||
# \brief Post processing plot displaying lines
|
||||
|
||||
import FreeCAD
|
||||
|
||||
from . import base_fempostextractors
|
||||
from . import base_fempythonobject
|
||||
_PropHelper = base_fempythonobject._PropHelper
|
||||
@@ -207,9 +209,9 @@ class PostIndexOverFrames2D(base_fempostextractors.Extractor2D):
|
||||
|
||||
frame_x_array.SetName("Frames")
|
||||
if frame_y_array.GetNumberOfComponents() > 1:
|
||||
frame_y_array.SetName(f"{obj.YField} ({obj.YComponent})")
|
||||
frame_y_array.SetName(f"{obj.YField} ({obj.YComponent}) @Idx {obj.Index}")
|
||||
else:
|
||||
frame_y_array.SetName(obj.YField)
|
||||
frame_y_array.SetName(f"{obj.YField} @Idx {obj.Index}")
|
||||
|
||||
table.AddColumn(frame_x_array)
|
||||
self._y_array_component_to_table(obj, frame_y_array, table)
|
||||
|
||||
@@ -29,17 +29,11 @@ __url__ = "https://www.freecad.org"
|
||||
# \ingroup FEM
|
||||
# \brief Post processing plot displaying histograms
|
||||
|
||||
from . import base_fempythonobject
|
||||
_PropHelper = base_fempythonobject._PropHelper
|
||||
|
||||
from . import base_fempostextractors
|
||||
from . import base_fempostvisualizations
|
||||
from . import post_extract1D
|
||||
|
||||
from vtkmodules.vtkCommonCore import vtkDoubleArray
|
||||
from vtkmodules.vtkCommonDataModel import vtkTable
|
||||
|
||||
|
||||
from femguiutils import post_visualization
|
||||
|
||||
# register visualization and extractors
|
||||
@@ -85,6 +79,7 @@ class PostHistogramFieldData(post_extract1D.PostFieldData1D):
|
||||
"""
|
||||
VisualizationType = "Histogram"
|
||||
|
||||
|
||||
class PostHistogramIndexOverFrames(post_extract1D.PostIndexOverFrames1D):
|
||||
"""
|
||||
A 1D index extraction for histogram.
|
||||
@@ -96,57 +91,11 @@ class PostHistogram(base_fempostvisualizations.PostVisualization):
|
||||
"""
|
||||
A post processing plot for showing extracted data as histograms
|
||||
"""
|
||||
|
||||
VisualizationType = "Histogram"
|
||||
|
||||
def __init__(self, obj):
|
||||
super().__init__(obj)
|
||||
obj.addExtension("App::GroupExtensionPython")
|
||||
|
||||
def _get_properties(self):
|
||||
prop = [
|
||||
_PropHelper(
|
||||
type="Fem::PropertyPostDataObject",
|
||||
name="Table",
|
||||
group="Base",
|
||||
doc="The data table that stores the plotted data, one column per histogram",
|
||||
value=vtkTable(),
|
||||
),
|
||||
]
|
||||
return super()._get_properties() + prop
|
||||
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
|
||||
if prop == "Group":
|
||||
# check if all objects are allowed
|
||||
|
||||
children = obj.Group
|
||||
for child in obj.Group:
|
||||
if not is_histogram_extractor(child):
|
||||
FreeCAD.Console.PrintWarning(f"{child.Label} is not a data histogram data extraction object, cannot be added")
|
||||
children.remove(child)
|
||||
|
||||
if len(obj.Group) != len(children):
|
||||
obj.Group = children
|
||||
|
||||
def execute(self, obj):
|
||||
|
||||
# during execution we collect all child data into our table
|
||||
table = vtkTable()
|
||||
for child in obj.Group:
|
||||
|
||||
c_table = child.Table
|
||||
for i in range(c_table.GetNumberOfColumns()):
|
||||
c_array = c_table.GetColumn(i)
|
||||
# TODO: check which array type it is and use that one
|
||||
array = vtkDoubleArray()
|
||||
array.DeepCopy(c_array)
|
||||
array.SetName(f"{child.Source.Label}: {c_array.GetName()}")
|
||||
table.AddColumn(array)
|
||||
|
||||
obj.Table = table
|
||||
return False
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -29,17 +29,10 @@ __url__ = "https://www.freecad.org"
|
||||
# \ingroup FEM
|
||||
# \brief Post processing plot displaying lines
|
||||
|
||||
from . import base_fempythonobject
|
||||
_PropHelper = base_fempythonobject._PropHelper
|
||||
|
||||
from . import base_fempostextractors
|
||||
from . import base_fempostvisualizations
|
||||
from . import post_extract2D
|
||||
|
||||
from vtkmodules.vtkCommonCore import vtkDoubleArray
|
||||
from vtkmodules.vtkCommonDataModel import vtkTable
|
||||
|
||||
|
||||
from femguiutils import post_visualization
|
||||
|
||||
# register visualization and extractors
|
||||
@@ -85,6 +78,7 @@ class PostLineplotFieldData(post_extract2D.PostFieldData2D):
|
||||
"""
|
||||
VisualizationType = "Lineplot"
|
||||
|
||||
|
||||
class PostLineplotIndexOverFrames(post_extract2D.PostIndexOverFrames2D):
|
||||
"""
|
||||
A 2D index extraction for lineplot.
|
||||
@@ -97,56 +91,7 @@ class PostLineplot(base_fempostvisualizations.PostVisualization):
|
||||
"""
|
||||
A post processing plot for showing extracted data as line plots
|
||||
"""
|
||||
|
||||
VisualizationType = "Lineplot"
|
||||
|
||||
def __init__(self, obj):
|
||||
super().__init__(obj)
|
||||
obj.addExtension("App::GroupExtensionPython")
|
||||
|
||||
def _get_properties(self):
|
||||
prop = [
|
||||
_PropHelper(
|
||||
type="Fem::PropertyPostDataObject",
|
||||
name="Table",
|
||||
group="Base",
|
||||
doc="The data table that stores the plotted data, two columns per lineplot (x,y)",
|
||||
value=vtkTable(),
|
||||
),
|
||||
]
|
||||
return super()._get_properties() + prop
|
||||
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
|
||||
if prop == "Group":
|
||||
# check if all objects are allowed
|
||||
|
||||
children = obj.Group
|
||||
for child in obj.Group:
|
||||
if not is_lineplot_extractor(child):
|
||||
FreeCAD.Console.PrintWarning(f"{child.Label} is not a data lineplot data extraction object, cannot be added")
|
||||
children.remove(child)
|
||||
|
||||
if len(obj.Group) != len(children):
|
||||
obj.Group = children
|
||||
|
||||
def execute(self, obj):
|
||||
|
||||
# during execution we collect all child data into our table
|
||||
table = vtkTable()
|
||||
for child in obj.Group:
|
||||
|
||||
c_table = child.Table
|
||||
for i in range(c_table.GetNumberOfColumns()):
|
||||
c_array = c_table.GetColumn(i)
|
||||
# TODO: check which array type it is and use that one
|
||||
array = vtkDoubleArray()
|
||||
array.DeepCopy(c_array)
|
||||
array.SetName(f"{child.Source.Label}: {c_array.GetName()}")
|
||||
table.AddColumn(array)
|
||||
|
||||
obj.Table = table
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -21,191 +21,76 @@
|
||||
# * *
|
||||
# ***************************************************************************
|
||||
|
||||
__title__ = "FreeCAD post line plot"
|
||||
__title__ = "FreeCAD post table"
|
||||
__author__ = "Stefan Tröger"
|
||||
__url__ = "https://www.freecad.org"
|
||||
|
||||
## @package post_lineplot
|
||||
## @package post_table
|
||||
# \ingroup FEM
|
||||
# \brief Post processing plot displaying lines
|
||||
# \brief Post processing plot displaying tables
|
||||
|
||||
from . import base_fempythonobject
|
||||
_PropHelper = base_fempythonobject._PropHelper
|
||||
|
||||
# helper function to extract plot object type
|
||||
def _get_extraction_subtype(obj):
|
||||
if hasattr(obj, 'Proxy') and hasattr(obj.Proxy, "Type"):
|
||||
return obj.Proxy.Type
|
||||
|
||||
return "unknown"
|
||||
from . import base_fempostextractors
|
||||
from . import base_fempostvisualizations
|
||||
from . import post_extract1D
|
||||
|
||||
|
||||
class PostLinePlot(base_fempythonobject.BaseFemPythonObject):
|
||||
from femguiutils import post_visualization
|
||||
|
||||
# register visualization and extractors
|
||||
post_visualization.register_visualization("Table",
|
||||
":/icons/FEM_PostSpreadsheet.svg",
|
||||
"ObjectsFem",
|
||||
"makePostTable")
|
||||
|
||||
post_visualization.register_extractor("Table",
|
||||
"TableFieldData",
|
||||
":/icons/FEM_PostField.svg",
|
||||
"1D",
|
||||
"Field",
|
||||
"ObjectsFem",
|
||||
"makePostTableFieldData")
|
||||
|
||||
|
||||
post_visualization.register_extractor("Table",
|
||||
"TableIndexOverFrames",
|
||||
":/icons/FEM_PostIndex.svg",
|
||||
"1D",
|
||||
"Index",
|
||||
"ObjectsFem",
|
||||
"makePostTableIndexOverFrames")
|
||||
|
||||
# Implementation
|
||||
# ##############
|
||||
|
||||
def is_table_extractor(obj):
|
||||
|
||||
if not base_fempostextractors.is_extractor_object(obj):
|
||||
return False
|
||||
|
||||
if not hasattr(obj.Proxy, "VisualizationType"):
|
||||
return False
|
||||
|
||||
return obj.Proxy.VisualizationType == "Table"
|
||||
|
||||
|
||||
class PostTableFieldData(post_extract1D.PostFieldData1D):
|
||||
"""
|
||||
A post processing extraction for plotting lines
|
||||
A 1D Field extraction for tables.
|
||||
"""
|
||||
|
||||
Type = "App::FeaturePython"
|
||||
|
||||
def __init__(self, obj):
|
||||
super().__init__(obj)
|
||||
obj.addExtension("App::GroupExtension")
|
||||
self._setup_properties(obj)
|
||||
|
||||
def _setup_properties(self, obj):
|
||||
|
||||
self.ExtractionType = "LinePlot"
|
||||
|
||||
pl = obj.PropertiesList
|
||||
for prop in self._get_properties():
|
||||
if not prop.Name in pl:
|
||||
prop.add_to_object(obj)
|
||||
|
||||
def _get_properties(self):
|
||||
prop = []
|
||||
return prop
|
||||
|
||||
def onDocumentRestored(self, obj):
|
||||
self._setup_properties(self, obj):
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
|
||||
if prop == "Group":
|
||||
# check if all objects are allowed
|
||||
|
||||
children = obj.Group
|
||||
for child in obj.Group:
|
||||
if _get_extraction_subtype(child) not in ["Line"]:
|
||||
children.remove(child)
|
||||
|
||||
if len(obj.Group) != len(children):
|
||||
obj.Group = children
|
||||
VisualizationType = "Table"
|
||||
|
||||
|
||||
class PostPlotLine(base_fempythonobject.BaseFemPythonObject):
|
||||
class PostTableIndexOverFrames(post_extract1D.PostIndexOverFrames1D):
|
||||
"""
|
||||
A 1D index extraction for table.
|
||||
"""
|
||||
VisualizationType = "Table"
|
||||
|
||||
Type = "App::FeaturePython"
|
||||
|
||||
def __init__(self, obj):
|
||||
super().__init__(obj)
|
||||
self._setup_properties(obj)
|
||||
class PostTable(base_fempostvisualizations.PostVisualization):
|
||||
"""
|
||||
A post processing plot for showing extracted data as tables
|
||||
"""
|
||||
VisualizationType = "Table"
|
||||
|
||||
def _setup_properties(self, obj):
|
||||
|
||||
self.ExtractionType = "Line"
|
||||
|
||||
pl = obj.PropertiesList
|
||||
for prop in self._get_properties():
|
||||
if not prop.Name in pl:
|
||||
prop.add_to_object(obj)
|
||||
|
||||
def _get_properties(self):
|
||||
|
||||
prop = [
|
||||
_PropHelper(
|
||||
type="App::PropertyLink",
|
||||
name="Source",
|
||||
group="Line",
|
||||
doc="The data source, the line uses",
|
||||
value=None,
|
||||
),
|
||||
_PropHelper(
|
||||
type="App::PropertyEnumeration",
|
||||
name="XField",
|
||||
group="X Data",
|
||||
doc="The field to use as X data",
|
||||
value=None,
|
||||
),
|
||||
_PropHelper(
|
||||
type="App::PropertyEnumeration",
|
||||
name="XComponent",
|
||||
group="X Data",
|
||||
doc="Which part of the X field vector to use for the X axis",
|
||||
value=None,
|
||||
),
|
||||
_PropHelper(
|
||||
type="App::PropertyEnumeration",
|
||||
name="YField",
|
||||
group="Y Data",
|
||||
doc="The field to use as Y data for the line plot",
|
||||
value=None,
|
||||
),
|
||||
_PropHelper(
|
||||
type="App::PropertyEnumeration",
|
||||
name="YComponent",
|
||||
group="Y Data",
|
||||
doc="Which part of the Y field vector to use for the X axis",
|
||||
value=None,
|
||||
),
|
||||
]
|
||||
return prop
|
||||
|
||||
def onDocumentRestored(self, obj):
|
||||
self._setup_properties(self, obj):
|
||||
|
||||
def onChanged(self, obj, prop):
|
||||
|
||||
if prop == "Source":
|
||||
# check if the source is a Post object
|
||||
if obj.Source and not obj.Source.isDerivedFrom("Fem::FemPostObject"):
|
||||
FreeCAD.Console.PrintWarning("Invalid object: Line source must be FemPostObject")
|
||||
obj.XField = []
|
||||
obj.YField = []
|
||||
obj.Source = None
|
||||
|
||||
if prop == "XField":
|
||||
if not obj.Source:
|
||||
obj.XComponent = []
|
||||
return
|
||||
|
||||
point_data = obj.Source.Data.GetPointData()
|
||||
if not point_data.HasArray(obj.XField):
|
||||
obj.XComponent = []
|
||||
return
|
||||
|
||||
match point_data.GetArray(fields.index(obj.XField)).GetNumberOfComponents:
|
||||
case 1:
|
||||
obj.XComponent = ["Not a vector"]
|
||||
case 2:
|
||||
obj.XComponent = ["Magnitude", "X", "Y"]
|
||||
case 3:
|
||||
obj.XComponent = ["Magnitude", "X", "Y", "Z"]
|
||||
|
||||
if prop == "YField":
|
||||
if not obj.Source:
|
||||
obj.YComponent = []
|
||||
return
|
||||
|
||||
point_data = obj.Source.Data.GetPointData()
|
||||
if not point_data.HasArray(obj.YField):
|
||||
obj.YComponent = []
|
||||
return
|
||||
|
||||
match point_data.GetArray(fields.index(obj.YField)).GetNumberOfComponents:
|
||||
case 1:
|
||||
obj.YComponent = ["Not a vector"]
|
||||
case 2:
|
||||
obj.YComponent = ["Magnitude", "X", "Y"]
|
||||
case 3:
|
||||
obj.YComponent = ["Magnitude", "X", "Y", "Z"]
|
||||
|
||||
def onExecute(self, obj):
|
||||
# we need to make sure that we show the correct fields to the user as option for data extraction
|
||||
|
||||
fields = []
|
||||
if obj.Source:
|
||||
point_data = obj.Source.Data.GetPointData()
|
||||
fields = [point_data.GetArrayName(i) for i in range(point_data.GetNumberOfArrays())]
|
||||
|
||||
current_X = obj.XField
|
||||
obj.XField = fields
|
||||
if current_X in fields:
|
||||
obj.XField = current_X
|
||||
|
||||
current_Y = obj.YField
|
||||
obj.YField = fields
|
||||
if current_Y in fields:
|
||||
obj.YField = current_Y
|
||||
|
||||
return True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user