-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add verification report for adapy in ci/cd pipeline
- Loading branch information
Showing
10 changed files
with
443 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import logging | ||
from typing import Dict, Union | ||
|
||
import pandas as pd | ||
from common import append_df, eig_data_to_df, make_eig_fem | ||
from paradoc import OneDoc | ||
from paradoc.common import TableFormat | ||
|
||
from ada import Beam, Material | ||
from ada.materials.metals import CarbonSteel | ||
|
||
|
||
def shorten_name(name, fem_format, geom_repr) -> str: | ||
short_name = name.replace("cantilever_EIG_", "") | ||
short_name_map = dict(calculix="ccx", code_aster="ca", abaqus="aba", sesam="ses") | ||
geom_repr_map = dict(solid="so", line="li", shell="sh") | ||
short_name = short_name.replace(fem_format, short_name_map[fem_format]) | ||
short_name = short_name.replace(geom_repr, geom_repr_map[geom_repr]) | ||
|
||
return short_name | ||
|
||
|
||
def run_and_postprocess(bm, soft, geo, elo, df_write_map, results, overwrite, execute, eig_modes): | ||
res = make_eig_fem(bm, soft, geo, elo, overwrite=overwrite, execute=execute, eigen_modes=eig_modes) | ||
if res is None or res.eigen_mode_data is None: | ||
logging.error("No result file is located") | ||
return None | ||
else: | ||
short_name = shorten_name(res.name, soft, geo) | ||
df = eig_data_to_df(res.eigen_mode_data, ["Mode", short_name]) | ||
df_current = df_write_map.get(geo) | ||
if df_current is not None: | ||
df = df[short_name] | ||
df_write_map[geo] = append_df(df, df_current) | ||
results.append(res) | ||
|
||
|
||
def simulate(bm, el_order, geom_repr, analysis_software, eig_modes, overwrite, execute): | ||
results = [] | ||
merged_line_df = None | ||
merged_shell_df = None | ||
merged_solid_df = None | ||
|
||
df_write_map: Dict[str, Union[pd.DataFrame, None]] = dict( | ||
line=merged_line_df, shell=merged_shell_df, solid=merged_solid_df | ||
) | ||
|
||
for elo in el_order: | ||
for geo in geom_repr: | ||
for soft in analysis_software: | ||
try: | ||
run_and_postprocess(bm, soft, geo, elo, df_write_map, results, overwrite, execute, eig_modes) | ||
except IOError as e: | ||
logging.error(e) | ||
return results, df_write_map | ||
|
||
|
||
def main(overwrite, execute): | ||
analysis_software = ["calculix", "code_aster"] | ||
el_order = [1, 2] | ||
geom_repr = ["line", "shell", "solid"] | ||
eig_modes = 11 | ||
|
||
bm = Beam( | ||
"MyBeam", | ||
(0, 0.5, 0.5), | ||
(3, 0.5, 0.5), | ||
"IPE400", | ||
Material("S420", CarbonSteel("S420")), | ||
) | ||
|
||
one = OneDoc("report") | ||
|
||
one.variables = dict( | ||
geom_specifics=str(bm), | ||
ca_version=14.2, | ||
ccx_version=2.16, | ||
aba_version=2021, | ||
ses_version=10, | ||
num_modes=eig_modes, | ||
) | ||
|
||
table_format = TableFormat(font_size=8, float_fmt=".3f") | ||
|
||
results, df_write_map = simulate(bm, el_order, geom_repr, analysis_software, eig_modes, overwrite, execute) | ||
|
||
for geo in geom_repr: | ||
one.add_table( | ||
f"eig_compare_{geo}", | ||
df_write_map[geo], | ||
f"Comparison of all Eigenvalue analysis using {geo} elements", | ||
tbl_format=table_format, | ||
) | ||
|
||
for res in results: | ||
one.add_table( | ||
res.name, | ||
eig_data_to_df(res.eigen_mode_data, ["Mode", "Eigenvalue (real)"]), | ||
res.name, | ||
) | ||
|
||
one.compile("ADA-FEA-verification") | ||
|
||
|
||
if __name__ == "__main__": | ||
main(overwrite=False, execute=False) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import logging | ||
import os | ||
import pathlib | ||
|
||
import pandas as pd | ||
|
||
from ada import Assembly, Beam, Material, Part | ||
from ada.config import Settings | ||
from ada.fem import Bc, FemSet, Load, StepEigen, StepImplicit | ||
from ada.fem.concepts.eigenvalue import EigenDataSummary | ||
from ada.fem.exceptions import FEASolverNotInstalled, IncompatibleElements | ||
from ada.fem.formats.utils import default_fem_res_path | ||
from ada.fem.meshing.concepts import GmshOptions, GmshSession | ||
from ada.fem.results import Results | ||
from ada.fem.utils import get_beam_end_nodes, get_eldata | ||
from ada.materials.metals import CarbonSteel | ||
|
||
|
||
def static_cantilever(): | ||
beam = Beam( | ||
"MyBeam", | ||
(0, 0.5, 0.5), | ||
(3, 0.5, 0.5), | ||
"IPE400", | ||
Material("S420", CarbonSteel("S420")), | ||
) | ||
|
||
p = Part("MyPart") | ||
a = Assembly("MyAssembly") / [p / beam] | ||
|
||
p.fem = beam.to_fem_obj(0.1, "shell", options=GmshOptions(Mesh_ElementOrder=2)) | ||
|
||
fix_set = p.fem.add_set(FemSet("bc_nodes", get_beam_end_nodes(beam), FemSet.TYPES.NSET)) | ||
|
||
load_set = p.fem.add_set(FemSet("load_node", get_beam_end_nodes(beam, 2), FemSet.TYPES.NSET)) | ||
a.fem.add_bc(Bc("Fixed", fix_set, [1, 2, 3, 4, 5, 6])) | ||
step = a.fem.add_step(StepImplicit("StaticStep")) | ||
step.add_load(Load("MyLoad", Load.TYPES.FORCE, -15e3, load_set, 3)) | ||
return a | ||
|
||
|
||
def make_eig_fem( | ||
beam: Beam, | ||
fem_format, | ||
geom_repr, | ||
elem_order=1, | ||
incomplete_2nd_order=True, | ||
overwrite=True, | ||
execute=True, | ||
eigen_modes=10, | ||
): | ||
name = f"cantilever_EIG_{fem_format}_{geom_repr}_o{elem_order}" | ||
scratch_dir = Settings.scratch_dir / "eigen_fem" | ||
fem_res_files = default_fem_res_path(name, scratch_dir=scratch_dir) | ||
|
||
p = Part("MyPart") | ||
a = Assembly("MyAssembly") / [p / beam] | ||
os.makedirs("temp", exist_ok=True) | ||
if fem_res_files[fem_format].exists() is False: | ||
execute = True | ||
overwrite = True | ||
|
||
mesh_size = 0.05 if geom_repr != "line" else 0.3 | ||
|
||
if overwrite is True: | ||
gmsh_opt = GmshOptions(Mesh_ElementOrder=elem_order, Mesh_MeshSizeMin=mesh_size) | ||
gmsh_opt.Mesh_SecondOrderIncomplete = 1 if incomplete_2nd_order is True else 0 | ||
p.fem = beam.to_fem_obj(mesh_size, geom_repr, options=gmsh_opt) | ||
fs = p.fem.add_set(FemSet("bc_nodes", beam.bbox.sides.back(return_fem_nodes=True))) | ||
a.fem.add_bc(Bc("Fixed", fs, [1, 2, 3, 4, 5, 6])) | ||
|
||
a.fem.add_step(StepEigen("EigenStep", num_eigen_modes=eigen_modes)) | ||
|
||
try: | ||
res = a.to_fem( | ||
name, | ||
fem_format, | ||
overwrite=overwrite, | ||
execute=execute, | ||
scratch_dir=scratch_dir, | ||
) | ||
except IncompatibleElements as e: | ||
logging.error(e) | ||
return None | ||
except FEASolverNotInstalled as e: | ||
logging.error(e) | ||
return None | ||
except BaseException as e: | ||
raise Exception(e) | ||
if res.output is not None: | ||
os.makedirs("temp/logs", exist_ok=True) | ||
with open(f"temp/logs/{name}.log", "w") as f: | ||
f.write(res.output.stdout) | ||
|
||
if res.eigen_mode_data is not None: | ||
for eig in res.eigen_mode_data.modes: | ||
print(eig) | ||
else: | ||
logging.error("Result file not created") | ||
|
||
assert pathlib.Path(res.results_file_path).exists() | ||
return res | ||
|
||
|
||
def make_2nd_order_complete_elements(): | ||
overwrite = False | ||
execute = False | ||
fem_format = "code_aster" | ||
elem_order = 2 | ||
beam = Beam( | ||
"MyBeam", | ||
(0, 0.5, 0.5), | ||
(3, 0.5, 0.5), | ||
"IPE400", | ||
Material("S420", CarbonSteel("S420")), | ||
) | ||
geom_repr = "solid" | ||
gmsh_opt = GmshOptions(Mesh_ElementOrder=elem_order, Mesh_SecondOrderIncomplete=0) | ||
|
||
a = Assembly("MyAssembly") / [Part("MyPart") / beam] | ||
|
||
with GmshSession(silent=False, options=gmsh_opt) as gs: | ||
gs.add_obj(beam, geom_repr=geom_repr) | ||
gs.options.Mesh_Algorithm = 6 | ||
gs.options.Mesh_Algorithm3D = 10 | ||
# gs.open_gui() | ||
gs.mesh(0.05) if geom_repr != "line" else gs.mesh(1.0) | ||
a.get_part("MyPart").fem = gs.get_fem() | ||
print(get_eldata(a)) | ||
fix_set = a.get_part("MyPart").fem.add_set(FemSet("bc_nodes", get_beam_end_nodes(beam), FemSet.TYPES.NSET)) | ||
a.fem.add_bc(Bc("Fixed", fix_set, [1, 2, 3, 4, 5, 6])) | ||
a.fem.add_step(StepEigen("Eigen", num_eigen_modes=10)) | ||
|
||
name = f"cantilever_EIG_{fem_format}_{geom_repr}_o{elem_order}" | ||
res = a.to_fem(name, fem_format, overwrite=overwrite, execute=execute) | ||
|
||
if res.eigen_mode_data is not None: | ||
for eig in res.eigen_mode_data.modes: | ||
print(eig) | ||
|
||
assert pathlib.Path(res.results_file_path).exists() | ||
return res | ||
|
||
|
||
def append_df(new_df, old_df): | ||
if old_df is None: | ||
updated_df = new_df | ||
else: | ||
updated_df = pd.concat([old_df, new_df], axis=1) | ||
return updated_df | ||
|
||
|
||
def eig_data_to_df(eig_data: EigenDataSummary, columns): | ||
return pd.DataFrame([(e.no, e.f_hz) for e in eig_data.modes], columns=columns) | ||
|
||
|
||
def eig_result_to_table(res: Results): | ||
eig_data = res.eigen_mode_data | ||
df = pd.DataFrame([(e.no, e.f_hz) for e in eig_data.modes], columns=["Mode", "Eigenvalue (real)"]) | ||
return df.to_markdown(index=False, tablefmt="grid") | ||
|
||
|
||
if __name__ == "__main__": | ||
a = static_cantilever() | ||
scratch = Settings.scratch_dir / "ada-testing" | ||
opts = dict(execute=True, overwrite=True, scratch_dir=scratch) | ||
a.to_fem("MyCantileverLoadTest_sesam", "sesam", **opts) | ||
a.to_fem("MyCantileverLoadTest_abaqus", "abaqus", **opts) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
# Open Source FEA using adapy | ||
|
||
This report describes the main verification work of the simulation results produced by the | ||
Open Source Finite Element (FE) solvers [Code Aster](https://www.code-aster.org/spip.php?rubrique2) | ||
and [Calculix](http://www.dhondt.de/). | ||
|
||
The motivation behind producing this report is to increase confidence in the conversion of FEM models in _adapy_ | ||
and also the results from the various FEA solvers. | ||
|
||
## Introduction | ||
|
||
The results from _Code Aster_ and _Calculix_ and tools will be compared with proprietary FEA solvers such as; | ||
|
||
|
||
* [Abaqus](https://www.3ds.com/products-services/simulia/products/abaqus/) | ||
* [Sestra (part of DNV's Sesam suite)](https://www.dnv.com/services/linear-structural-analysis-sestra-2276) | ||
|
||
|
||
The simulations are pre- and post processed using [adapy](https://github.com/Krande/adapy) and this report is | ||
automatically generated using [paradoc](https://github.com/Krande/paradoc) as | ||
part of a ci/cd pipeline within the _adapy_ repositories. | ||
|
||
For each new addition of code affecting the handling of | ||
FEM in _adapy_ a new series of analysis is performed whereas the results are gathered in this auto-generated document. | ||
|
||
|
||
## FEA Solvers | ||
|
||
* Calculix v{{__ccx_version__}} | ||
* Code Aster v{{__ca_version__}} | ||
* Abaqus v{{__aba_version__}} | ||
* Sestra v{{__ses_version__}} | ||
|
||
|
||
## Python packages | ||
The following python packages were instrumental in the creation of this document and the FEA results herein. | ||
|
||
### Adapy | ||
|
||
The intention behind _adapy_ is to make it easier to work with finite element models | ||
and BIM models. | ||
|
||
### Paradoc | ||
|
||
_paradoc_ was created to simplify the generation of reports by creating the structure of the document and text | ||
in markdown with a string substitution scheme that lets you easily pass in tables, functions and finally be | ||
able to produce production ready documents in Microsoft Word. |
Oops, something went wrong.