FreeCAD / src /Mod /Fem /femtest /app /support_utils.py
AbdulElahGwaith's picture
Upload folder using huggingface_hub
985c397 verified
# ***************************************************************************
# * Copyright (c) 2018 Bernd Hahnebach <bernd@bimstatik.org> *
# * *
# * This file is part of the FreeCAD CAx development system. *
# * *
# * 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__ = "Tools for FEM unit tests"
__author__ = "Bernd Hahnebach"
__url__ = "https://www.freecad.org"
import math
import os
import sys
import tempfile
import unittest
from typing import Union, List, Iterator
import FreeCAD
from os.path import join
def get_fem_test_home_dir():
return join(FreeCAD.getHomePath(), "Mod", "Fem", "femtest", "data")
def get_fem_test_tmp_dir(dirname=None):
from uuid import uuid4
_unique_id = str(uuid4())[-12:]
# print(_unique_id)
if dirname is None:
temp_dir = join(tempfile.gettempdir(), "FEM_unittests", _unique_id)
else:
temp_dir = join(tempfile.gettempdir(), "FEM_unittests", dirname + "_" + _unique_id)
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
return temp_dir
def get_unit_test_tmp_dir(temp_dir, unittestdir):
testdir = join(temp_dir, unittestdir)
if not os.path.exists(testdir):
os.makedirs(testdir)
return testdir
def fcc_print(message):
FreeCAD.Console.PrintMessage(f"{message} \n")
def get_namefromdef(strdel="", stradd=""):
# https://code.activestate.com/recipes/66062-determining-current-function-name/
return (sys._getframe(1).f_code.co_name).replace(strdel, stradd)
def get_defmake_count(fem_vtk_post=True):
"""
count the def make in module ObjectsFem
could also be done in bash with
grep -c "def make" src/Mod/Fem/ObjectsFem.py
"""
name_modfile = join(FreeCAD.getHomePath(), "Mod", "Fem", "ObjectsFem.py")
modfile = open(name_modfile)
lines_modefile = modfile.readlines()
modfile.close()
lines_defmake = [li for li in lines_modefile if li.startswith("def make")]
if not fem_vtk_post:
# FEM VTK post processing is disabled
# we are not able to create VTK post objects
new_lines = []
for li in lines_defmake:
if "Post" not in li:
new_lines.append(li)
lines_defmake = new_lines
return len(lines_defmake)
def get_fem_test_defs():
test_path = join(FreeCAD.getHomePath(), "Mod", "Fem", "femtest", "app")
print(f"Modules, classes, methods taken from: {test_path}")
collected_test_module_paths = []
for tfile in sorted(os.listdir(test_path)):
if tfile.startswith("test") and tfile.endswith(".py"):
collected_test_module_paths.append(join(test_path, tfile))
collected_test_modules = []
collected_test_classes = []
collected_test_methods = []
for f in collected_test_module_paths:
module_name = os.path.splitext(os.path.basename(f))[0]
module_path = f"femtest.app.{module_name}"
if module_path not in collected_test_modules:
collected_test_modules.append(module_path)
class_name = ""
tfile = open(f)
for ln in tfile:
ln = ln.lstrip()
ln = ln.rstrip()
if ln.startswith("class "):
ln = ln.lstrip("class ")
ln = ln.split("(")[0]
class_name = ln
class_path = f"femtest.app.{module_name}.{class_name}"
if class_path not in collected_test_classes:
collected_test_classes.append(class_path)
if ln.startswith("def test"):
ln = ln.lstrip("def ")
ln = ln.split("(")[0]
if ln == "test_00print":
continue
method_path = f"femtest.app.{module_name}.{class_name}.{ln}"
collected_test_methods.append(method_path)
tfile.close()
# write to file
file_path = join(tempfile.gettempdir(), "test_commands.sh")
cf = open(file_path, "w")
cf.write("# created by Python\n")
cf.write("'''\n")
cf.write("from femtest.app.support_utils import get_fem_test_defs\n")
cf.write("get_fem_test_defs()\n")
cf.write("\n")
cf.write("\n")
cf.write("# all FEM App tests\n")
cf.write("make -j 4 && ./bin/FreeCAD --run-test 'TestFemApp'\n")
cf.write("\n")
cf.write("make -j 4 && ./bin/FreeCADCmd --run-test 'TestFemApp'\n")
cf.write("\n")
cf.write("\n")
cf.write("'''\n")
cf.write("\n")
cf.write("# modules\n")
for m in collected_test_modules:
cf.write(f"make -j 4 && ./bin/FreeCADCmd -t {m}\n")
cf.write("\n")
cf.write("\n")
cf.write("# classes\n")
for m in collected_test_classes:
cf.write(f"make -j 4 && ./bin/FreeCADCmd -t {m}\n")
cf.write("\n")
cf.write("\n")
cf.write("# methods\n")
for m in collected_test_methods:
cf.write(f"make -j 4 && ./bin/FreeCADCmd -t {m}\n")
cf.write("\n")
cf.write("\n")
cf.write("# methods in FreeCAD\n")
for m in collected_test_methods:
cf.write(
"\nimport unittest\n"
"unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(\n"
" '{}'\n"
"))\n".format(m)
)
cf.close()
print(f"The file was saved in:{file_path}")
def try_converting_to_float(line: str) -> Union[None, List[float]]:
"""Does its best to split a line and convert its elements to float
Has 3 strategies of splitting:
* by comma - mainly in CalculiX .inp files
* by space - other solvers
* by space and ignoring the 1st word - other solvers
If there was only 1 word, the line will fail, no compromises
Single characters always pass
:param line: line to split and convert to floats
:return: None if conversion failed, else list of floats
"""
strategies = [
lambda _line: _line[1:].split(","),
lambda _line: _line[1:].split(),
lambda _line: (lambda split_line: len(split_line) > 1 and split_line or "fail")(
_line[1:].split()[1:]
),
]
for strategy in strategies:
try:
return list(map(float, strategy(line)))
except ValueError:
pass
return None
def are_floats_equal(
orig_floats: Union[None, List[float]], floats_to_compare: Union[None, List[float]]
) -> bool:
"""Check if floats in lists are equal with some tolerance
:param orig_floats: list of floats - left operands of comparison
:param floats_to_compare: list of floats - right operands of comparison
:return: True if both lists are equal with some tolerance element-wise, else False
"""
if any(floats is None for floats in (orig_floats, floats_to_compare)):
return False
for orig_float, float_to_compare in zip(orig_floats, floats_to_compare):
if not math.isclose(orig_float, float_to_compare, abs_tol=1e-10):
return False
return True
def parse_diff(diff_lines: Iterator[str]) -> List[str]:
"""Parses lines from `united_diff`
Tries to split a line and convert its contents to float, to
compare float numbers with some tolerance
Recognizes blocks of changes, that start with `@@` and end either at EOF or
at the beginning of another block. Only bad lines are added to block.
All lines, which didn't pass the element-wise check, are bad lines.
:param diff_lines: lines produced by `united_diff`
:return: list of bad lines with respect to their block of change
"""
bad_lines = []
changes_block = []
while True:
try:
first = next(diff_lines)
if first.startswith("@@"):
if len(changes_block) > 1:
bad_lines.extend(changes_block)
changes_block = [first]
continue
second = next(diff_lines)
except StopIteration:
break
if first.startswith("---"):
continue
if not are_floats_equal(*map(try_converting_to_float, (first, second))):
changes_block.extend((first, second))
# check if we have any remaining block
if len(changes_block) > 1:
bad_lines.extend(changes_block)
return bad_lines
def compare_inp_files(file_name1, file_name2):
file1 = open(file_name1)
f1 = file1.readlines()
file1.close()
# l.startswith("17671.0,1") is a temporary workaround
# for python3 problem with 1DFlow input
# TODO as soon as the 1DFlow result reading is fixed
# this should be triggered in the 1DFlow unit test
lf1 = [
li
for li in f1
if not (
li.startswith("** written ")
or li.startswith("** file ")
or li.startswith("17671.0,1")
)
]
lf1 = force_unix_line_ends(lf1)
file2 = open(file_name2)
f2 = file2.readlines()
file2.close()
# TODO see comment on file1
lf2 = [
li
for li in f2
if not (
li.startswith("** written ")
or li.startswith("** file ")
or li.startswith("17671.0,1")
)
]
lf2 = force_unix_line_ends(lf2)
import difflib
diff_lines = difflib.unified_diff(lf1, lf2, n=0)
bad_lines = parse_diff(diff_lines)
if bad_lines:
return f"Comparing {file_name1} to {file_name2} failed!\n{''.join(bad_lines)}"
def compare_files(file_name1, file_name2):
file1 = open(file_name1)
f1 = file1.readlines()
file1.close()
# TODO: add support for variable values in the reference file
# instead of using this workaround
# workaround to compare geos of elmer test and temporary file path
# (not only names change, path changes with operating system)
lf1 = [
li
for li in f1
if not (
li.startswith('Merge "')
or li.startswith('Save "')
or li.startswith("// ")
or li.startswith("General.NumThreads")
)
]
lf1 = force_unix_line_ends(lf1)
file2 = open(file_name2)
f2 = file2.readlines()
file2.close()
lf2 = [
li
for li in f2
if not (
li.startswith('Merge "')
or li.startswith('Save "')
or li.startswith("// ")
or li.startswith("General.NumThreads")
)
]
lf2 = force_unix_line_ends(lf2)
import difflib
diff = difflib.unified_diff(lf1, lf2, n=0)
result = ""
for li in diff:
result += li
if result:
result = f"Comparing {file_name1} to {file_name2} failed!\n" + result
return result
def compare_stats(fea, stat_file, res_obj_name, loc_stat_types=None):
import femresult.resulttools as resulttools
# get the stat types which should be compared
stat_types = [
"U1",
"U2",
"U3",
"Uabs",
"Sabs",
"MaxPrin",
"MidPrin",
"MinPrin",
"MaxShear",
"Peeq",
"Temp",
"MFlow",
"NPress",
]
if not loc_stat_types:
loc_stat_types = stat_types
# get stats from result obj which should be compared
obj = fea.analysis.Document.getObject(res_obj_name)
# fcc_print(obj)
if obj:
# fcc_print(obj.Name)
stats = []
for s in loc_stat_types:
statval = resulttools.get_stats(obj, s)
stats.append(f"{s}: ({statval[0]:.10f}, {statval[1]:.10f})\n")
else:
fcc_print(f"Result object not found. Name: {res_obj_name}")
return True
# get stats to compare with, the expected ones
sf = open(stat_file)
sf_content = []
for li in sf.readlines():
for st in loc_stat_types:
if li.startswith(st):
sf_content.append(li)
sf.close()
sf_content = force_unix_line_ends(sf_content)
if sf_content == []:
return True
# compare stats
if stats != sf_content:
fcc_print(f"Stats read from {fea.base_name}.frd file")
fcc_print("!=")
fcc_print(f"Expected stats from {stat_file}")
for i in range(len(stats)):
if stats[i] != sf_content[i]:
fcc_print(f"{stats[i].rstrip()} != {sf_content[i].rstrip()}")
return True
return False
def force_unix_line_ends(line_list):
new_line_list = []
for ln in line_list:
if ln.endswith("\r\n"):
ln = ln[:-2] + "\n"
new_line_list.append(ln)
return new_line_list
def collect_python_modules(femsubdir=None):
if not femsubdir:
pydir = join(FreeCAD.ConfigGet("AppHomePath"), "Mod", "Fem")
else:
pydir = join(FreeCAD.ConfigGet("AppHomePath"), "Mod", "Fem", femsubdir)
collected_modules = []
fcc_print(pydir)
for pyfile in sorted(os.listdir(pydir)):
if pyfile.endswith(".py") and not pyfile.startswith("Init"):
if not femsubdir:
collected_modules.append(os.path.splitext(os.path.basename(pyfile))[0])
else:
collected_modules.append(
femsubdir.replace("/", ".")
+ "."
+ os.path.splitext(os.path.basename(pyfile))[0]
)
return collected_modules
def all_test_files():
# open all files
cube_frequency()
cube_static()
Flow1D_thermomech()
multimat()
spine_thermomech()
# run the specific test case of the file
# open the file in FreeCAD GUI and return the doc identifier
def cube_frequency():
testname = "femtest.testccxtools.TestCcxTools.test_3_freq_analysis"
unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(testname))
doc = FreeCAD.open(join(get_fem_test_tmp_dir(), "FEM_ccx_frequency", "cube_frequency.FCStd"))
return doc
def cube_static():
testname = "femtest.testccxtools.TestCcxTools.test_1_static_analysis"
unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(testname))
doc = FreeCAD.open(join(get_fem_test_tmp_dir(), "FEM_ccx_static", "cube_static.FCStd"))
return doc
def Flow1D_thermomech():
testname = "femtest.testccxtools.TestCcxTools.test_5_Flow1D_thermomech_analysis"
unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(testname))
doc = FreeCAD.open(
join(get_fem_test_tmp_dir(), "FEM_ccx_Flow1D_thermomech", "Flow1D_thermomech.FCStd")
)
return doc
def multimat():
testname = "femtest.testccxtools.TestCcxTools.test_2_static_multiple_material"
unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(testname))
doc = FreeCAD.open(join(get_fem_test_tmp_dir(), "FEM_ccx_multimat", "multimat.FCStd"))
return doc
def spine_thermomech():
testname = "femtest.testccxtools.TestCcxTools.test_4_thermomech_analysis"
unittest.TextTestRunner().run(unittest.TestLoader().loadTestsFromName(testname))
doc = FreeCAD.open(join(get_fem_test_tmp_dir(), "FEM_ccx_thermomech", "spine_thermomech.FCStd"))
return doc