| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | __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:] |
| | |
| | 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=""): |
| | |
| | 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: |
| | |
| | |
| | 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() |
| |
|
| | |
| | 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)) |
| | |
| | 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() |
| | |
| | |
| | |
| | |
| | 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() |
| | |
| | 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() |
| |
|
| | |
| | |
| |
|
| | |
| | |
| | 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 |
| |
|
| | |
| | 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 |
| |
|
| | |
| | obj = fea.analysis.Document.getObject(res_obj_name) |
| | |
| | if obj: |
| | |
| | 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 |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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(): |
| | |
| | cube_frequency() |
| | cube_static() |
| | Flow1D_thermomech() |
| | multimat() |
| | spine_thermomech() |
| |
|
| |
|
| | |
| | |
| | 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 |
| |
|