+ +
+ + +# Description + +FEM-Design is an advanced and intuitive structural analysis software. We support all aspects of your structural engineering requirements: from 3D modelling, design and finite element analysis (FEA) of concrete, steel, timber, composite, masonry and foundation structures. All calculations are performed to Eurocode standards, with some specific National annexes. + +The quick and easy nature of FEM-Design makes it an ideal choice for all types of construction tasks, from single element design to global stability analysis of large buildings, making it the best practical program for structural engineers to use for their day to day tasks. + + +## Scope + +The python package is mainly focus on [`fdscript`](https://femdesign-api-docs.onstrusoft.com/docs/advanced/fdscript) automation which will help you in automatise processes as running analysis, design and read results. + +The construction of the `Database` object is currently out of scope as it is delegated to the users. `Database` is based on `xml` sintax and you can use library such as `xml.etree.ElementTree` to manipulate the file. + +## Example + +```python +from femdesign.comunication import FemDesignConnection, Verbosity +from femdesign.calculate.command import DesignModule +from femdesign.calculate.analysis import Analysis, Design, CombSettings, CombItem + + +pipe = FemDesignConnection() +try: + pipe.SetVerbosity(Verbosity.SCRIPT_LOG_LINES) + pipe.Open(r"example/simple_beam.str") + pipe.RunAnalysis(Analysis.FrequencyAnalysis(num_shapes=5)) + pipe.Save(r"example\to_delete\simple_beam_out_2.str") + pipe.GenerateListTables(bsc_file=r"example\bsc\quantity-estimation-steel.bsc", + csv_file=r"example\output\quantity-estimation-steel.csv") + pipe.Exit() +except Exception as err: + pipe.KillProgramIfExists() + raise err +``` + +A wider list of examples can be found in [example](https://github.com/strusoft/femdesign-api/tree/master/FemDesign.Python/examples) + +## Documentation + + +https://femdesign-api-docs.onstrusoft.com/docs/intro \ No newline at end of file diff --git a/FemDesign.Python/packaging/assets/FD_logo.png b/FemDesign.Python/packaging/assets/FD_logo.png new file mode 100644 index 00000000..906bd098 Binary files /dev/null and b/FemDesign.Python/packaging/assets/FD_logo.png differ diff --git a/FemDesign.Python/packaging/dist/fem_design-0.0.1-py3-none-any.whl b/FemDesign.Python/packaging/dist/fem_design-0.0.1-py3-none-any.whl new file mode 100644 index 00000000..72bbba9d Binary files /dev/null and b/FemDesign.Python/packaging/dist/fem_design-0.0.1-py3-none-any.whl differ diff --git a/FemDesign.Python/packaging/dist/fem_design-0.0.1.tar.gz b/FemDesign.Python/packaging/dist/fem_design-0.0.1.tar.gz new file mode 100644 index 00000000..6abb55a9 Binary files /dev/null and b/FemDesign.Python/packaging/dist/fem_design-0.0.1.tar.gz differ diff --git a/FemDesign.Python/packaging/pyproject.toml b/FemDesign.Python/packaging/pyproject.toml new file mode 100644 index 00000000..7af84896 --- /dev/null +++ b/FemDesign.Python/packaging/pyproject.toml @@ -0,0 +1,23 @@ +[project] +name = "FEM-Design" +version = "0.0.2" +authors = [ + { name="FEM-Design", email="femdesign.api@strusoft.com" }, +] +maintainers = [ + {name = "Marco Pellegrino", email = "marco.pellegrino@strusoft.com"} +] +description = "The FEM-Design API package" +readme = "README.md" +requires-python = ">=3.8" +classifiers = [ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: Microsoft", +] +keywords = ["fem", "fea", "structures", "strusoft", "FEM-Design API"] + +[project.urls] +Homepage = "https://femdesign-api-docs.onstrusoft.com" +Repository = "https://github.com/strusoft/femdesign-api" +Issues = "https://github.com/strusoft/femdesign-api/issues" \ No newline at end of file diff --git a/FemDesign.Python/packaging/src/fem_design.egg-info/PKG-INFO b/FemDesign.Python/packaging/src/fem_design.egg-info/PKG-INFO new file mode 100644 index 00000000..67072818 --- /dev/null +++ b/FemDesign.Python/packaging/src/fem_design.egg-info/PKG-INFO @@ -0,0 +1,66 @@ +Metadata-Version: 2.1 +Name: fem-design +Version: 0.0.1 +Summary: The FEM-Design API package +Author-email: FEM-Design+ +
+ + +# Description + +FEM-Design is an advanced and intuitive structural analysis software. We support all aspects of your structural engineering requirements: from 3D modelling, design and finite element analysis (FEA) of concrete, steel, timber, composite, masonry and foundation structures. All calculations are performed to Eurocode standards, with some specific National annexes. + +The quick and easy nature of FEM-Design makes it an ideal choice for all types of construction tasks, from single element design to global stability analysis of large buildings, making it the best practical program for structural engineers to use for their day to day tasks. + + +## Scope + +The python package is mainly focus on [`fdscript`](https://femdesign-api-docs.onstrusoft.com/docs/advanced/fdscript) automation which will help you in automatise processes as running analysis, design and read results. + +The construction of the `Database` object is currently out of scope as it is delegated to the users. `Database` is based on `xml` sintax and you can use library such as `xml.etree.ElementTree` to manipulate the file. + +## Example + +```python +from femdesign.comunication import FemDesignConnection, Verbosity +from femdesign.calculate.command import DesignModule +from femdesign.calculate.analysis import Analysis, Design, CombSettings, CombItem + + +pipe = FemDesignConnection() +try: + pipe.SetVerbosity(Verbosity.SCRIPT_LOG_LINES) + pipe.Open(r"example/simple_beam.str") + pipe.RunAnalysis(Analysis.FrequencyAnalysis(num_shapes=5)) + pipe.Save(r"example\to_delete\simple_beam_out_2.str") + pipe.GenerateListTables(bsc_file=r"example\bsc\quantity-estimation-steel.bsc", + csv_file=r"example\output\quantity-estimation-steel.csv") + pipe.Exit() +except Exception as err: + pipe.KillProgramIfExists() + raise err +``` + +A wider list of examples can be found in [example](https://github.com/strusoft/femdesign-api/tree/master/FemDesign.Python/examples) + +## Documentation + + +https://femdesign-api-docs.onstrusoft.com/docs/intro diff --git a/FemDesign.Python/packaging/src/fem_design.egg-info/SOURCES.txt b/FemDesign.Python/packaging/src/fem_design.egg-info/SOURCES.txt new file mode 100644 index 00000000..0e273883 --- /dev/null +++ b/FemDesign.Python/packaging/src/fem_design.egg-info/SOURCES.txt @@ -0,0 +1,16 @@ +LICENSE +README.md +pyproject.toml +src/fem_design.egg-info/PKG-INFO +src/fem_design.egg-info/SOURCES.txt +src/fem_design.egg-info/dependency_links.txt +src/fem_design.egg-info/top_level.txt +src/femdesign/__init__.py +src/femdesign/comunication.py +src/femdesign/database.py +src/femdesign/calculate/__init__.py +src/femdesign/calculate/analysis.py +src/femdesign/calculate/command.py +src/femdesign/calculate/fdscript.py +src/femdesign/utilities/__init__.py +src/femdesign/utilities/filehelper.py \ No newline at end of file diff --git a/FemDesign.Python/packaging/src/fem_design.egg-info/dependency_links.txt b/FemDesign.Python/packaging/src/fem_design.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/FemDesign.Python/packaging/src/fem_design.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/FemDesign.Python/packaging/src/fem_design.egg-info/top_level.txt b/FemDesign.Python/packaging/src/fem_design.egg-info/top_level.txt new file mode 100644 index 00000000..c85eead1 --- /dev/null +++ b/FemDesign.Python/packaging/src/fem_design.egg-info/top_level.txt @@ -0,0 +1 @@ +femdesign diff --git a/FemDesign.Python/packaging/src/femdesign/__init__.py b/FemDesign.Python/packaging/src/femdesign/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/FemDesign.Python/packaging/src/femdesign/calculate/__init__.py b/FemDesign.Python/packaging/src/femdesign/calculate/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/FemDesign.Python/packaging/src/femdesign/calculate/analysis.py b/FemDesign.Python/packaging/src/femdesign/calculate/analysis.py new file mode 100644 index 00000000..0ddd087f --- /dev/null +++ b/FemDesign.Python/packaging/src/femdesign/calculate/analysis.py @@ -0,0 +1,418 @@ +from enum import Enum +import xml.etree.ElementTree as ET + + + +class Stage: + """ + Class to represent a stage in FEM-Design + """ + class Method(Enum): + """ + Enum to represent the calculation method of the construction stage + """ + TRACKING = 0 + GHOST = 1 + + def __init__(self, method : Method = Method.TRACKING, tda : bool = True, creepincrementlimit = 25.0): + self.method = method.value + self.tda = tda + self.creepincrementlimit = creepincrementlimit + + def to_xml_element(self) -> ET.Element: + """Convert the Stage object to an xml element + + Returns: + ET.Element: xml element representing the Stage object + """ + stage = ET.Element("stage") + stage.attrib = { + "ghost": str(self.method.value), + "tda": str(int(self.tda)), + "creepincrementlimit": str(self.creepincrementlimit) + } + return stage + + +class CombItem: + def __init__(self, Calc : bool = True, NLE : bool = False, PL : bool = False, NLS : bool = False, Cr : bool = False, f2nd : bool = False, Im : float = 0, Amplitudo : float = 1.0, Waterlevel : float = 0, ImpfRqd = 0, StabRqd = 0): + self.Calc = Calc + self.NLE = NLE + self.PL = PL + self.NLS = NLS + self.Cr = Cr + self.f2nd = f2nd + self.Im = Im + self.Amplitudo = Amplitudo + self.Waterlevel = Waterlevel + + self.ImpfRqd = ImpfRqd + self.StabRqd = StabRqd + + + @classmethod + def StaticAnalysis(cls, Calc : bool = True, NLE : bool = False, PL : bool = False): + return cls(Calc = Calc, NLE = NLE, PL = PL) + + @classmethod + def NoCalculation(cls): + return cls(Calc = False) + + def to_xml_element(self): + comb_item = ET.Element("combitem") + comb_item.attrib = { + "Calc": str(int(self.Calc)), + "NLE": str(int(self.NLE)), + "PL": str(int(self.PL)), + "NLS": str(int(self.NLS)), + "Cr": str(int(self.Cr)), + "f2nd": str(int(self.f2nd)), + "Im": str(self.Im), + "Amplitudo": str(self.Amplitudo), + "Waterlevel": str(self.Waterlevel), + + "ImpfRqd": str(self.ImpfRqd), + "StabRqd": str(self.StabRqd), + } + return comb_item + + +class CombSettings: + def __init__(self, NLEmaxiter : int = 30, PLdefloadstep : int = 20, PLminloadstep : int = 2, PLmaxeqiter : int = 30, PlKeepLoadStep : bool = True, PlTolerance : int = 1, PlShellLayers : int = 10, PlShellCalcStr : bool = 1, NLSMohr : bool = True, NLSinitloadstep : int = 10, NLSminloadstep : int = 10, NLSactiveelemratio : int = 5, NLSplasticelemratio : int = 5, CRloadstep : int = 20, CRmaxiter : int = 30, CRstifferror : int = 2, combitems : list[CombItem] = None): + self.NLEmaxiter = NLEmaxiter + self.PLdefloadstep = PLdefloadstep + self.PLminloadstep = PLminloadstep + self.PLmaxeqiter = PLmaxeqiter + self.PlKeepLoadStep = PlKeepLoadStep + self.PlTolerance = PlTolerance + self.PlShellLayers = PlShellLayers + self.PlShellCalcStr = PlShellCalcStr + self.NLSMohr = NLSMohr + self.NLSinitloadstep = NLSinitloadstep + self.NLSminloadstep = NLSminloadstep + self.NLSactiveelemratio = NLSactiveelemratio + self.NLSplasticelemratio = NLSplasticelemratio + self.CRloadstep = CRloadstep + self.CRmaxiter = CRmaxiter + self.CRstifferror = CRstifferror + self.combitems = combitems or [] + + @classmethod + def Default(cls): + return cls() + + def to_xml_element(self) -> ET.Element: + comb = ET.Element("comb") + comb.attrib = { + "NLEmaxiter": str(self.NLEmaxiter), + "PLdefloadstep": str(self.PLdefloadstep), + "PLminloadstep": str(self.PLminloadstep), + "PLmaxeqiter": str(self.PLmaxeqiter), + "PlKeepLoadStep": str(self.PlKeepLoadStep), + "PlTolerance": str(self.PlTolerance), + "PlShellLayers": str(self.PlShellLayers), + "PlShellCalcStr": str(self.PlShellCalcStr), + "NLSMohr": str(self.NLSMohr), + "NLSinitloadstep": str(self.NLSinitloadstep), + "NLSminloadstep": str(self.NLSminloadstep), + "NLSactiveelemratio": str(self.NLSactiveelemratio), + "NLSplasticelemratio": str(self.NLSplasticelemratio), + "CRloadstep": str(self.CRloadstep), + "CRmaxiter": str(self.CRmaxiter), + "CRstifferror": str(self.CRstifferror) + } + + for combitem in self.combitems: + comb.append(combitem.to_xml_element()) + return comb + + + +class Freq: + """ + Class to represent the frequency analysis settings + """ + class ShapeNormalization(Enum): + """ + Enum to represent the normalization unit of the shape + """ + MassMatrix = 0 + Unit = 1 + + def __init__(self, num_shapes : int = 2, auto_iter : int = 0, max_sturm : int = 0, norm_unit : ShapeNormalization = ShapeNormalization.MassMatrix, x : bool = True, y : bool = True, z : bool = True, top : float = -0.01): + self.Numshapes = num_shapes + self.AutoIter = auto_iter + self.MaxSturm = max_sturm + self.NormUnit = norm_unit + self.X = x + self.Y = y + self.Z = z + self.top = top + + def to_xml_element(self) -> ET.Element: + """ Convert the Freq object to an xml element + + Returns: + ET.Element: xml element representing the Freq object + """ + freq = ET.Element("freq") + freq.attrib = { + "Numshapes": str(self.Numshapes), + "MaxSturm": str(self.MaxSturm), + "NormUnit": str(self.NormUnit.value), + "X": str(int(self.X)), + "Y": str(int(self.Y)), + "Z": str(int(self.Z)), + "top": str(self.top), + "AutoIter": str(self.AutoIter) + } + return freq + + @classmethod + def Default(cls, num_shapes = 5, auto_iter = 0, max_sturm = 0, norm_unit = ShapeNormalization.MassMatrix, x = True, y = True, z = True, top = -0.01): + return cls(num_shapes, auto_iter, max_sturm, norm_unit, x, y, z, top) + +class Footfall: + def __init__(self, TopOfSubstructure : float = -0.01): + self.TopOfSubstructure = TopOfSubstructure + + def to_xml_element(self): + footfall = ET.Element("footfall") + footfall.attrib = { + "TopOfSubstructure": str(self.TopOfSubstructure) + } + return footfall + + +class ThGroundAcc: + def __init__(self, flevelspectra=1, dts=0.20, tsend=5.0, q=1.0, facc=1, nres=5, tcend=20.0, method=0, alpha=0.000, beta=0.000, ksi=5.0): + self.flevelspectra = flevelspectra + self.dts = dts + self.tsend = tsend + self.q = q + self.facc = facc + self.nres = nres + self.tcend = tcend + self.method = method + self.alpha = alpha + self.beta = beta + self.ksi = ksi + + def to_xml_element(self): + thgroundacc = ET.Element("thgroundacc") + thgroundacc.attrib = { + "flevelspectra": str(self.flevelspectra), + "dts": str(self.dts), + "tsend": str(self.tsend), + "q": str(self.q), + "facc": str(self.facc), + "nres": str(self.nres), + "tcend": str(self.tcend), + "method": str(self.method), + "alpha": str(self.alpha), + "beta": str(self.beta), + "ksi": str(self.ksi) + } + return thgroundacc + + +class ThExForce: + def __init__(self, nres=5, tcend=20.0, method=0, alpha=0.000, beta=0.000, ksi=5.0): + self.nres = nres + self.tcend = tcend + self.method = method + self.alpha = alpha + self.beta = beta + self.ksi = ksi + + def to_xml_element(self): + thexforce = ET.Element("thexforce") + thexforce.attrib = { + "nres": str(self.nres), + "tcend": str(self.tcend), + "method": str(self.method), + "alpha": str(self.alpha), + "beta": str(self.beta), + "ksi": str(self.ksi) + } + return thexforce + + +class PeriodicExc: + def __init__(self, deltat=0.0100, tend=5.00, dampeningtype=0, alpha=0.000, beta=0.000, ksi=5.0): + self.deltat = deltat + self.tend = tend + self.dampeningtype = dampeningtype + self.alpha = alpha + self.beta = beta + self.ksi = ksi + + def to_xml_element(self): + periodicexc = ET.Element("periodicexc") + periodicexc.attrib = { + "deltat": str(self.deltat), + "tend": str(self.tend), + "dampeningtype": str(self.dampeningtype), + "alpha": str(self.alpha), + "beta": str(self.beta), + "ksi": str(self.ksi) + } + return periodicexc + + +class Design: + def __init__(self, autodesign : bool = True, check : bool = True, load_combination : bool = True): + self.autodesign = autodesign + self.check = check + self.load_combination = load_combination + + def to_xml_element(self): + design = ET.Element("design") + + + if self.load_combination: + comb_element = ET.SubElement(design, "cmax") + else: + comb_element = ET.SubElement(design, "gmax") + comb_element.text = "" + + autodesign_element = ET.SubElement(design, "autodesign") + autodesign_element.text = str(self.autodesign).lower() + + check_element = ET.SubElement(design, "check") + check_element.text = str(self.check).lower() + return design + +class Analysis: + def __init__(self, + calcCase : bool = False, + calcComb : bool = False, + calcGmax : bool = False, + calcStage : bool = False, + calcImpf : bool = False, + calcStab : bool = False, + calcFreq : bool = False, + calcSeis : bool = False, + calcFootfall : bool = False, + calcMovingLoad : bool = False, + calcThGrounAcc : bool = False, + calcThExforce : bool = False, + calcPeriodicExc : bool = False, + calcStoreyFreq : bool = False, + calcBedding : bool = False, + calcDesign : bool = False, + elemfine : bool = True, + diaphragm : bool = False, + peaksmoothings : bool = False, + comb : CombSettings = None, + stage : Stage = None, + freq : Freq = None, + footfall : Footfall = None, + #bedding=None, + thgroundacc : ThGroundAcc = None, + thexforce : ThExForce = None, + periodicexc : PeriodicExc = None): + self.calcCase = calcCase + self.calcStage = calcStage + self.calcImpf = calcImpf + self.calcComb = calcComb + self.calcGmax = calcGmax + self.calcStab = calcStab + self.calcFreq = calcFreq + self.calcSeis = calcSeis + self.calcFootfall = calcFootfall + self.calcMovingLoad = calcMovingLoad + self.calcBedding = calcBedding + self.calcThGrounAcc = calcThGrounAcc + self.calcThExforce = calcThExforce + self.calcPeriodicExc = calcPeriodicExc + self.calcStoreyFreq = calcStoreyFreq + self.calcDesign = calcDesign + + self.elemfine = elemfine + self.diaphragm = diaphragm + self.peaksmoothings = peaksmoothings + + self.stage = stage + self.comb = comb + self.freq = freq + self.footfall = footfall + #self.bedding = bedding + self.thgroundacc = thgroundacc + self.thexforce = thexforce + self.periodicexc = periodicexc + + def to_xml_element(self): + analysis = ET.Element("analysis") + + analysis.attrib = { + "calcCase": str(int(self.calcCase)), + "calcComb": str(int(self.calcComb)), + "calcGmax": str(int(self.calcGmax)), + "calcStage": str(int(self.calcStage)), + "calcImpf": str(int(self.calcImpf)), + "calcStab": str(int(self.calcStab)), + "calcFreq": str(int(self.calcFreq)), + "calcSeis": str(int(self.calcSeis)), + "calcFootfall": str(int(self.calcFootfall)), + "calcMovingLoad": str(int(self.calcMovingLoad)), + "calcThGrounAcc": str(int(self.calcThGrounAcc)), + "calcThExforce": str(int(self.calcThExforce)), + "calcPeriodicExc": str(int(self.calcPeriodicExc)), + "calcStoreyFreq": str(int(self.calcStoreyFreq)), + "calcBedding": str(int(self.calcBedding)), + "calcDesign": str(int(self.calcDesign)), + + "elemfine": str(int(self.elemfine)), + "diaphragm": str(int(self.diaphragm)), + "peaksmoothings": str(int(self.peaksmoothings)) + } + + if self.comb: + analysis.append(self.comb.to_xml_element()) + if self.stage: + analysis.append(self.stage.to_xml_element()) + if self.freq: + analysis.append(self.freq.to_xml_element()) + if self.footfall: + analysis.append(self.footfall.to_xml_element()) + if self.thgroundacc: + analysis.append(self.thgroundacc.to_xml_element()) + if self.thexforce: + analysis.append(self.thexforce.to_xml_element()) + if self.periodicexc: + analysis.append(self.periodicexc.to_xml_element()) + # if self.bedding: + # analysis.append(self.bedding.to_xml_element()) + + return analysis + + @classmethod + def StaticAnalysis(cls, comb : CombSettings = CombSettings.Default(), calcCase : bool = True, calcComb : bool = True): + return cls(calcCase = calcCase, calcComb = calcComb, comb = comb) + + @classmethod + def FrequencyAnalysis(cls, num_shapes : int = 5, auto_iter : int = 0, max_sturm : int = 0, norm_unit : Freq.ShapeNormalization = Freq.ShapeNormalization.MassMatrix, x : bool = True, y : bool = True, z : bool = True, top : bool = -0.01): + freq = Freq(num_shapes, auto_iter, max_sturm, norm_unit, x, y, z, top) + return cls(calcFreq = True, freq = freq) + + @classmethod + def FootfallAnalysis(cls, footfall : Footfall, calcFootfall : bool = True): + return cls(calcFootfall = calcFootfall, footfall = footfall) + +# class Bedding: +# def __init__(self, Ldcomb="a", Meshprep=0, Stiff_X=0.5, Stiff_Y=0.5): +# self.Ldcomb = Ldcomb +# self.Meshprep = Meshprep +# self.Stiff_X = Stiff_X +# self.Stiff_Y = Stiff_Y + +# def to_xml_element(self): +# bedding = ET.Element("bedding") +# bedding.attrib = { +# "Ldcomb": str(self.Ldcomb), +# "Meshprep": str(self.Meshprep), +# "Stiff_X": str(self.Stiff_X), +# "Stiff_Y": str(self.Stiff_Y) +# } +# return bedding \ No newline at end of file diff --git a/FemDesign.Python/packaging/src/femdesign/calculate/command.py b/FemDesign.Python/packaging/src/femdesign/calculate/command.py new file mode 100644 index 00000000..59a347a9 --- /dev/null +++ b/FemDesign.Python/packaging/src/femdesign/calculate/command.py @@ -0,0 +1,445 @@ +import os +import xml.etree.ElementTree as ET +from enum import Enum, auto +from abc import ABC, abstractmethod + +from .analysis import Analysis, Design + +import uuid +import pathlib + +class User(Enum): + """Enum class to represent the different modules in FEM-Design + """ + STRUCT = auto() + LOADS = auto() + MESH = auto() + RESMODE = auto() + FOUNDATIONDESIGN = auto() + RCDESIGN = auto() + STEELDESIGN = auto() + TIMBERDESIGN = auto() + MASONRYDESIGN = auto() + COMPOSITEDESIGN = auto() + PERFORMANCEBASEDDESIGN = auto() + + +class DesignModule(Enum): + """Enum class to represent the different design modules in FEM-Design + """ + RCDESIGN = auto() + STEELDESIGN = auto() + TIMBERDESIGN = auto() + MASONRYDESIGN = auto() + COMPOSITEDESIGN = auto() + + def to_user(self) -> User: + """Convert the DesignModule to a User object + + Returns: + User: User object corresponding to the DesignModule + """ + mapping = { + DesignModule.RCDESIGN: User.RCDESIGN, + DesignModule.STEELDESIGN: User.STEELDESIGN, + DesignModule.TIMBERDESIGN: User.TIMBERDESIGN, + DesignModule.MASONRYDESIGN: User.MASONRYDESIGN, + DesignModule.COMPOSITEDESIGN: User.COMPOSITEDESIGN + } + return mapping[self] + + +class Command(ABC): + """Abstract class to represent a command in a FEM-Design script file + """ + @abstractmethod + def to_xml_element(self) -> ET.Element: + pass + + +class CmdUser(Command): + """CmdUser class to represent the fdscript 'cmduser' command + """ + def __init__(self, module : User): + self.module = module + + def to_xml_element(self) -> ET.Element: + """convert the CmdUser object to an xml element + + Returns: + ET.Element: xml element representing the CmdUser object + """ + cmd_user = ET.Element("cmduser") + cmd_user.attrib = {"command": f"; CXL $MODULE {self.module.name}"} + + return cmd_user + + @classmethod + def ResMode(cls) -> Command: + """Create a CmdUser object for the ResMode module + + Returns: + Command: CmdUser object for the ResMode module + """ + return cls(User.RESMODE) + + @classmethod + def Load(cls) -> Command: + """Create a CmdUser object for the Load module + + Returns: + Command: CmdUser object for the Load module + """ + return cls(User.LOADS) + + @classmethod + def Mesh(cls) -> Command: + """Create a CmdUser object for the Mesh module + + Returns: + Command: CmdUser object for the Mesh module + """ + return cls(User.MESH) + + @classmethod + def FoundationDesign(cls) -> Command: + """Create a CmdUser object for the FoundationDesign module + + Returns: + Command: CmdUser object for the FoundationDesign module + """ + return cls(User.FOUNDATIONDESIGN) + + @classmethod + def RCDesign(cls) -> Command: + """Create a CmdUser object for the RCDesign module + + Returns: + Command: CmdUser object for the RCDesign module + """ + return cls(User.RCDESIGN) + + @classmethod + def SteelDesign(cls) -> Command: + """Create a CmdUser object for the SteelDesign module + + Returns: + Command: CmdUser object for the SteelDesign module + """ + return cls(User.STEELDESIGN) + + @classmethod + def TimberDesign(cls)-> Command: + """Create a CmdUser object for the TimberDesign module + + Returns: + Command: CmdUser object for the TimberDesign module + """ + return cls(User.TIMBERDESIGN) + + @classmethod + def MasonryDesign(cls) -> Command: + """Create a CmdUser object for the MasonryDesign module + + Returns: + Command: CmdUser object for the MasonryDesign module + """ + return cls(User.MASONRYDESIGN) + + @classmethod + def CompositeDesign(cls) -> Command: + """Create a CmdUser object for the CompositeDesign module + + Returns: + Command: CmdUser object for the CompositeDesign module + """ + return cls(User.COMPOSITEDESIGN) + + @classmethod + def PerformanceBasedDesign(cls) -> Command: + """Create a CmdUser object for the PerformanceBasedDesign module + + Returns: + Command: CmdUser object for the PerformanceBasedDesign module + """ + return cls(User.PERFORMANCEBASEDDESIGN) + + @classmethod + def _fromDesignModule(cls, module : DesignModule) -> Command: + """Create a CmdUser object from a DesignModule + + Args: + module (DesignModule): DesignModule to create the CmdUser object from + + Returns: + Command: CmdUser object for the specified DesignModule + """ + if module == DesignModule.RCDESIGN: + return cls.RCDesign() + if module == DesignModule.STEELDESIGN: + return cls.SteelDesign() + if module == DesignModule.TIMBERDESIGN: + return cls.TimberDesign() + if module == DesignModule.MASONRYDESIGN: + return cls.MasonryDesign() + if module == DesignModule.COMPOSITEDESIGN: + return cls.CompositeDesign() + + +class CmdOpen(Command): + """CmdOpen class to represent the fdscript 'cmdopen' command + """ + def __init__(self, file_name : str): + """Constructor for the CmdOpen class + + Args: + file_name (str): path to the file to open + """ + self.file_name = os.path.abspath(file_name) + + def to_xml_element(self) -> ET.Element: + cmd_open = ET.Element("cmdopen") + + file_name_elem = ET.SubElement(cmd_open, "filename") + file_name_elem.text = self.file_name + + attributes = {"command": "; CXL CS2SHELL OPEN"} + cmd_open.attrib = attributes + + return cmd_open + + +class CmdCalculation(Command): + """CmdCalculation class to represent the fdscript 'cmdcalculation' command + """ + def __init__(self, analysis : Analysis, design : Design = None): + """Constructor for the CmdCalculation class + + Args: + analysis (Analysis): Analysis object to be included in the calculation + design (Design, optional): Design object to be included in the calculation. + """ + self.analysis = analysis + self.design = design + + + def to_xml_element(self) -> ET.Element: + """Convert the CmdCalculation object to an xml element + + Returns: + ET.Element: xml element representing the CmdCalculation object + """ + cmd_calculation = ET.Element("cmdcalculation") + + attributes = { "command": "; CXL $MODULE CALC"} + cmd_calculation.attrib = attributes + + if self.analysis: + cmd_calculation.append(self.analysis.to_xml_element()) + if self.design: + cmd_calculation.append(self.design.to_xml_element()) + + return cmd_calculation + + +class CmdSave(Command): + """CmdSave class to represent the fdscript 'cmdsave' command + """ + def __init__(self, file_name : str): + """Constructor for the CmdSave class + + Args: + file_name (str): path to the file to save + """ + + if not file_name.endswith(".str") and not file_name.endswith(".struxml"): + raise ValueError("file_name must have suffix .str or .struxml") + + if not os.path.exists(os.path.dirname(os.path.abspath(file_name))): + os.makedirs(os.path.dirname(os.path.abspath(file_name))) + + self.file_name = os.path.abspath(file_name) + + def to_xml_element(self) -> ET.Element: + """Convert the CmdSave object to an xml element + + Returns: + ET.Element: xml element representing the CmdSave object + """ + cmd_save = ET.Element("cmdsave") + + file_name_elem = ET.SubElement(cmd_save, "filename") + file_name_elem.text = self.file_name + + attributes = {"command": "; CXL CS2SHELL SAVE"} + cmd_save.attrib = attributes + + return cmd_save + + +class CmdEndSession(Command): + """CmdEndSession class to represent the fdscript 'cmdendsession' command + """ + def __init__(self): + """""" + pass + + def to_xml_element(self) -> ET.Element: + """convert the CmdEndSession object to an xml element + + Returns: + ET.Element: xml element representing the CmdEndSession object + """ + cmd_end_session = ET.Element("cmdendsession") + return cmd_end_session + + +class CmdProjDescr(Command): + """class to represent the fdscript cmdprojdescr command + """ + def __init__(self, project : str, description : str, designer : str, signature : str, comment : str, items : dict = None, read : bool = False, reset : bool = False): + """Constructor for the CmdProjDescr class + + Args: + project (str): project name + description (str): description + designer (str): designer + signature (str): signature + comment (str): comment + items (dict, optional): define key-value user data. Defaults to None. + read (bool, optional): read the project settings. Value will be store in the clipboard. Defaults to False. + reset (bool, optional): reset the project settings. Defaults to False. + """ + self.project = project + self.description = description + self.designer = designer + self.signature = signature + self.comment = comment + self.items = items + self.read = read + self.reset = reset + + def to_xml_element(self) -> ET.Element: + """Convert the CmdProjDescr object to an xml element + + Returns: + ET.Element: xml element representing the CmdProjDescr object + """ + cmd_proj_descr = ET.Element("cmdprojdescr") + + attributes = { + "command": "$ MODULECOM PROJDESCR", + "szProject": self.project, + "szDescription": self.description, + "szDesigner": self.designer, + "szSignature": self.signature, + "szComment": self.comment, + "read": str(int(self.read)), + "reset": str(int(self.reset)) + } + + ## join two dictionaries + cmd_proj_descr.attrib = attributes + + if self.items: + for key, value in self.items.items(): + item = ET.SubElement(cmd_proj_descr, "item") + item.attrib = {"id": key, "txt": value} + + return cmd_proj_descr + + +class CmdSaveDocx(Command): + """class to represent the fdscript cmdsavedocx command + """ + def __init__(self, file_name : str): + """Constructor for the CmdSaveDocx class + + Args: + file_name (str): path to the file to save + """ + if not file_name.endswith(".docx"): + raise ValueError("file_name must have suffix .docx") + + if not os.path.exists(os.path.dirname(os.path.abspath(file_name))): + os.makedirs(os.path.dirname(os.path.abspath(file_name))) + + self.file_name = os.path.abspath(file_name) + + def to_xml_element(self) -> ET.Element: + """Convert the CmdSaveDocx object to an xml element + + Returns: + ET.Element: xml element representing the CmdSaveDocx object + """ + cmd_save_docx = ET.Element("cmdsavedocx") + + file_name_elem = ET.SubElement(cmd_save_docx, "filename") + file_name_elem.text = self.file_name + + attributes = {"command": "$ DOC SAVEDOCX"} + cmd_save_docx.attrib = attributes + + return cmd_save_docx + + + + +class CmdListGen: + """Class to represent the fdscript 'cmdlistgen' command + """ + def __init__(self, bscfile : str, outfile : str = None, guids : list[uuid.UUID] = None, regional : bool = True, fillcells : bool = True, headers : bool = True): + """Constructor for the CmdListGen class + + Args: + bscfile (str): path to the bsc file + outfile (str, optional): path to the output file. Defaults to None. + guids (list[uuid.UUID], optional): list of element part guids to include in the output. Defaults to None. + regional (bool, optional): + fillcells (bool, optional): + headers (bool, optional): + """ + if not bscfile.endswith(".bsc"): + raise ValueError("bscfile must have suffix .bsc") + + if outfile and not outfile.endswith(".csv"): + raise ValueError("outfile must have suffix .csv") + + if not outfile: + outfile = pathlib.Path(self.bscfile).with_suffix(".csv") + + if not os.path.exists(os.path.dirname(os.path.abspath(outfile))): + os.makedirs(os.path.dirname(os.path.abspath(outfile))) + + self.bscfile = os.path.abspath(bscfile) + self.outfile = os.path.abspath(outfile) + self.regional = regional + self.fillcells = fillcells + self.headers = headers + self.guids = guids + + def to_xml_element(self) -> ET.Element: + """convert the CmdListGen object to an xml element + + Returns: + ET.Element: xml element representing the CmdListGen object + """ + cmd_listgen = ET.Element("cmdlistgen") + + attributes = { + "command": "$ MODULECOM LISTGEN", + "bscfile": self.bscfile, + "outfile": self.outfile, + "regional": str(int(self.regional)), + "fillcells": str(int(self.fillcells)), + "headers": str(int(self.headers)), + } + + cmd_listgen.attrib = attributes + + if self.guids: + for guid in self.guids: + guid_elem = ET.SubElement(cmd_listgen, "GUID") + guid_elem.text = str(guid) + + return cmd_listgen \ No newline at end of file diff --git a/FemDesign.Python/packaging/src/femdesign/calculate/fdscript.py b/FemDesign.Python/packaging/src/femdesign/calculate/fdscript.py new file mode 100644 index 00000000..d59bd3ed --- /dev/null +++ b/FemDesign.Python/packaging/src/femdesign/calculate/fdscript.py @@ -0,0 +1,87 @@ +## create a template for a class in python +import os +import xml.etree.ElementTree as ET +from .command import Command + +class FdscriptHeader: + """ + Class to represent the header of a FEM-Design script file + """ + def __init__(self, log_file : str, title : str = "FEM-Design API", version : int = 2400, module : str = "SFRAME"): + self.title = title + self.version = str(version) + self.module = module + self.logfile = os.path.abspath(log_file) + + + def to_xml_element(self) -> ET.Element: + """Convert the FdscriptHeader object to an xml element + + Returns: + ET.Element: xml element representing the FdscriptHeader object + """ + fdscript_header = ET.Element("fdscriptheader") + + title_elem = ET.SubElement(fdscript_header, "title") + title_elem.text = self.title + + version_elem = ET.SubElement(fdscript_header, "version") + version_elem.text = self.version + + module_elem = ET.SubElement(fdscript_header, "module") + module_elem.text = self.module + + logfile_elem = ET.SubElement(fdscript_header, "logfile") + logfile_elem.text = self.logfile + + + return fdscript_header + + +class Fdscript: + """ + Class to represent a FEM-Design script file + """ + attributes = { + "xmlns:xsi" : "http://www.w3.org/2001/XMLSchema-instance", + "xsi:noNamespaceSchemaLocation" : "fdscript.xsd" + } + + def __init__(self, log_file : str, commands : list[Command] ): + self.fdscriptheader = FdscriptHeader(log_file) + self.commands = commands + + def add_command(self, command : Command): + """Add a command to the Fdscript object + + Args: + command (Command): Command object to add to the Fdscript object + """ + self.commands.append(command) + + def to_xml_element(self) -> ET.Element: + """Convert the Fdscript object to an xml element + + Returns: + ET.Element: xml element representing the Fdscript object + """ + fdscript = ET.Element("fdscript") + fdscript.attrib = self.attributes + + fdscript.append(self.fdscriptheader.to_xml_element()) + + for command in self.commands: + fdscript.append(command.to_xml_element()) + + return fdscript + + def serialise_to_file(self, file_name : str): + """Serialise the Fdscript object to a file + + Args: + file_name (str): file name to save the fdscript to + """ + fdscript = self.to_xml_element() + + tree = ET.ElementTree(fdscript) + tree.write(file_name, encoding="utf-8") \ No newline at end of file diff --git a/FemDesign.Python/packaging/src/femdesign/comunication.py b/FemDesign.Python/packaging/src/femdesign/comunication.py new file mode 100644 index 00000000..b3fbc8be --- /dev/null +++ b/FemDesign.Python/packaging/src/femdesign/comunication.py @@ -0,0 +1,454 @@ +import subprocess +from datetime import datetime +from time import sleep + +from enum import Enum +from femdesign.calculate.analysis import Analysis, Design +from femdesign.calculate.fdscript import Fdscript +from femdesign.utilities.filehelper import OutputFileHelper +from femdesign.calculate.command import DesignModule, CmdUser, CmdCalculation, CmdListGen, CmdOpen, CmdProjDescr, CmdSave + +import win32file +import win32pipe + +import os + +""" +FEM - Design usage with pipe + +To initiate : +1 : create a WIN32 named pipe for duplex mode, message oriented +1a : optional : create another pipe for back channel, named appending 'b'. +2 : launch FD with command line +/ p Name +passing the name you used at creation.FD will open it right at start and exit if can't +after successful open it listens to commands while the usual interface is active +you can combine it with the windowless / minimized mode to hide the window +it also attaches to the back channel pipe at this moment, if unable, all output is permanently disabled +3 : send commands through the pipe +4 : FD will exit if 'exit' command received or the pipe is closed on this end + +FD only reads the main pipe and only writes the back channel(if supplied), allowing this end to never +read.While the pipe is duplexand can be used in both direction, if it gets clogged in +one direction(by not reading what the other end sends), the write can get blocked too. +The document recommends using another pipe for a back channel. +By default nothing is written to the back channel, you need to set output level or commands with implicit reply. +FD buffers all outgoing messages till they can be sent over, if this end is lazy to read it will not clog, +however they will accumulate in memory. + + + +Messages are text in 9bit ANSI(codepage), limited to 4096 bytes + +The command format is +[!]cmd[space][args] +there is no delimiter at the end, the pipe message counts. +FD reads the pipe immediatelyand puts the commands in a queue.The queue is processed when it's READYSTATE +for another command, finishing execution of the previous or a running script. +The !requests out - of bound execution.That is not suppoerted by very command and mainly +serves to manipulate the queue itself, verbosity or check the communicaiton is alive. + +Commands: + +exit +stop the FD process + +detach +close the pipe and continue IN normal interface mode + +clear[in | out] +flush the FD mesage queue for the direction, both without parameters +has no Effect on what is already issued to the pipe + +echo[txt] +write txt to output + +stat +write queueand processing status to output + +v[N] +set verbosity control (bits) + 1: enable basic output + 2: echo all INPUT commands + 4: FD log Lines + 8: script log lines + *16: calculation window messages (except fortran) + *32: progress window title + + echo and stat always cretes output, otherwise nothing is written aT V = 0 + * not yet supported + +run[scriptfile] +execute script as from tools / run script menu +*Note: When using Unicode commands, add the suffix 'UTF8'. E.g.: "runUTF8 [scriptfile]". + +cmd[command] +execute command as if typed into the command window -- No warranty!!! +*Note: When using Unicode commands, add the suffix 'UTF8'. E.g.: "cmdUTF8 [command]". + +esc +press Escape during calculation to break it + +""" + + +def GetElapsedTime(start_time): + return (datetime.now() - start_time).total_seconds() + + +class _FdConnect: + + def __exit__(self, exc_type, exc_value, traceback): + self.Detach() + self.ClosePipe() + + def __init__(self, pipe_name="FdPipe1"): + """ + Creating fd pipe + One fd connection for the life of the fem design until closure. + It could be more than one with given uniqe pipe_name. + """ + self.pipe_name = pipe_name + self.pipe_send = self._CreatePipe(pipe_name) + self.pipe_read = self._CreatePipe(f"{pipe_name}b") + self.start_time = None + self._log_message_history = [] + + @staticmethod + def _CreatePipe(name): + return win32pipe.CreateNamedPipe( + r"\\.\pipe\%s" % name, + win32pipe.PIPE_ACCESS_DUPLEX, + win32pipe.PIPE_TYPE_MESSAGE + | win32pipe.PIPE_READMODE_MESSAGE + | win32pipe.PIPE_NOWAIT, + win32pipe.PIPE_UNLIMITED_INSTANCES, + 4096, + 4096, + 0, + None, + ) + + def _ConnectPipe(self, timeout=60): + start_time = datetime.now() + while GetElapsedTime(start_time) <= timeout: + try: + win32pipe.ConnectNamedPipe(self.pipe_send, None) + win32pipe.ConnectNamedPipe(self.pipe_read, None) + return + except: + sleep(0.5) + raise TimeoutError(f"Program not connected in {timeout} second") + + def Start(self, fd_path, timeout=60): + """ + Start and connect of fem-design with specified path + """ + self.process = subprocess.Popen([fd_path, "/p", self.pipe_name]) + self._ConnectPipe(timeout=timeout) + + def _Read(self): + """ + Read one message if exists + """ + try: + return win32file.ReadFile(self.pipe_read, 4096)[1].decode() + except Exception as err: + if err.winerror == 232: # Error text (The pipe is being closed.) Pipe is empty + return None + elif err.winerror == 109: # Error text (The pipe has been ended.) Conecction lost + raise AssertionError("Connection lost") + else: + raise err + + def ReadAll(self): + """ + Read all existing message + """ + results = [] + while True: + result = self._Read() + if result == None: + return "\n".join(results) + results.append(result) + self._log_message_history.append(result) + + def GetLogMessageHistory(self): + """ + Return all collected message + """ + return self._log_message_history + + def Send(self, message): + """ + Send one message + """ + try: + win32file.WriteFile(self.pipe_send, message.encode()) + except Exception as err: + if err.winerror == 232: # Error text (The pipe is being closed.) Connection lost + raise AssertionError("Connection lost") + else: + raise err + + def SendAndReceive(self, message, timeout=None): + """ + Complete workflow of send message and receive answer until timeout + """ + self.Send(message) + start_time = self.start_time or datetime.now() + while timeout == None or GetElapsedTime(start_time) <= timeout: + result = self.ReadAll() + if result: + return result + sleep(0.5) + raise TimeoutError(f"Program not responding after {timeout} second") + + def Stat(self, timeout=None): + """ + Return queueand processing status + """ + return self.SendAndReceive("stat", timeout=timeout) + + def LogLevel(self, n): + """ + Set log level + verbosity control (bits) + 1: enable basic output + 2: echo all INPUT commands + 4: FD log Lines + 8: script log lines + *16: calculation window messages (except fortran) + *32: progress window title + + echo and stat always creates output, otherwise nothing is written aT V = 0 + * not yet supported + """ + return self.Send(f"v {n}") + + def RunScript(self, path, timeout=None): + """ + Execute script as from tools / run script menu + """ + self.Send(f"runUTF8 {path}") + self.start_time = datetime.now() + while timeout == None or GetElapsedTime(self.start_time) <= timeout: + sleep(0.1) + if "script idle" in self.Stat(timeout=timeout): + self.start_time = None + return + raise TimeoutError(f"Too long script run time after {timeout} second") + + def Cmd(self, cmd_text): + """ + Execute command as if typed into the command window -- No warranty!!! + """ + self.Send(f"cmdUTF8 {cmd_text}") + + def Esc(self): + """ + Press Escape during calculation to break it + """ + self.Send("esc") + + def Exit(self): + """ + Close fem-design + """ + self.Send("exit") + + def Detach(self): + """ + Disconnect from pipe and close it. You can't reattach to fem-design. + It must be outside of KillProgramIfExists scope. + """ + self.Send("detach") + sleep(2) + self.ClosePipe() + + def ClosePipe(self): + """ + Regular closing of fd pipe + """ + win32file.CloseHandle(self.pipe_send) + win32file.CloseHandle(self.pipe_read) + + def KillProgramIfExists(self): + """ + Kill program if exists + """ + if not (self.process.poll()): + try: + self.process.kill() + except WindowsError: + print("Program gone in meantime") + + +class Verbosity(Enum): + BASIC = 1 + ECHO_ALL_INPUT_COMMANDS = 2 + FD_LOG_LINES = 4 + SCRIPT_LOG_LINES = 8 + CALCULATION_WINDOW_MESSAGES = 16 + PROGRESS_WINDOW_TITLE = 32 + + +## define a private class + + +class FemDesignConnection(_FdConnect): + def __init__(self, + fd_path : str = r"C:\Program Files\StruSoft\FEM-Design 23\fd3dstruct.exe", + pipe_name : str ="FdPipe1", + verbose : Verbosity = Verbosity.SCRIPT_LOG_LINES, + output_dir : str = None, + minimized : bool = False,): + super().__init__(pipe_name) + + self._output_dir = output_dir + + os.environ["FD_NOLOGO"] = "1" + + if minimized: + os.environ["FD_NOGUI"] = "1" + + self.Start(fd_path) + self.LogLevel(verbose) + + @property + def output_dir(self): + if self._output_dir == None: + return os.path.join(os.getcwd(), "FEM-Design API") + else: + return os.path.abspath(self._output_dir) + + @output_dir.setter + def output_dir(self, value): + self._output_dir = os.path.abspath(value) + if not os.path.exists(value): + os.makedirs(os.path.abspath(value)) + + + def RunScript(self, fdscript : Fdscript, file_name : str = "script"): + """ + + Args: + fdscript (Fdscript): fdscript object to run + file_name (str, optional): file name to save the script. Defaults to "script". + """ + path = OutputFileHelper.GetFdscriptFilePath(self.output_dir, file_name) + + + fdscript.serialise_to_file(path) + super().RunScript(path) + + def RunAnalysis(self, analysis : Analysis): + """Run analysis + + Args: + analysis (analysis.Analysis): analysis object + """ + log = OutputFileHelper.GetLogFilePath(self.output_dir) + + fdscript = Fdscript(log, [CmdUser.ResMode(), CmdCalculation(analysis)]) + self.RunScript(fdscript, "analysis") + + def RunDesign(self, designMode : DesignModule , design : Design): + """Run design + + Args: + designMode (DesignModule): design module + design (analysis.Design): design object + """ + log = OutputFileHelper.GetLogFilePath(self.output_dir) + + fdscript = Fdscript(log, [CmdUser(designMode.to_user()), CmdCalculation(design)]) + self.RunScript(fdscript, "design") + + def SetProjectDescription(self, project_name : str = "", project_description : str = "", designer : str = "", signature : str = "", comment : str = "", additional_info : dict = None, read : bool = False, reset : bool = False): + """Set project description + + Args: + project_name (str): project name + project_description (str): project description + designer (str): designer + signature (str): signature + comment (str): comment + additional_info (dict): define key-value user data. Defaults to None. + read (bool, optional): read the project settings. Value will be store in the clipboard. Defaults to False. + reset (bool, optional): reset the project settings. Defaults to False. + + Examples + -------- + >>> pipe = FemDesignConnection(fd_path= r"C:\Program Files\StruSoft\FEM-Design 23\\fd3dstruct.exe", + minimized= False) + >>> pipe.SetProjectDescription(project_name="Amazing project", + project_description="Created through Python", + designer="Marco Pellegrino Engineer", + signature="MP", + comment="Wish for the best", + project_parameters={"italy": "amazing", "sweden": "amazing_too"}) + + """ + log = OutputFileHelper.GetLogFilePath(self.output_dir) + + cmd_project = CmdProjDescr(project_name, project_description, designer, signature, comment, additional_info, read, reset) + fdscript = Fdscript(log, [cmd_project]) + self.RunScript(fdscript, "project_description") + + def Save(self, file_name : str): + """Save the project + + Args: + file_name (str): name of the file to save + + Examples + -------- + >>> pipe = FemDesignConnection(fd_path= "C:\Program Files\StruSoft\FEM-Design 23\\fd3dstruct.exe", + minimized= False) + >>> pipe.Save(r"outputFile.str") + >>> pipe.Save(r"outputFile.struxml") + """ + log = OutputFileHelper.GetLogFilePath(self.output_dir) + + cmd_save = CmdSave(file_name) + + fdscript = Fdscript(log, [cmd_save]) + self.RunScript(fdscript, "save") + + def Open(self, file_name : str): + """Open a model from file + + Args: + file_name (str): file path to open + + Raises: + ValueError: extension must be .struxml or .str + FileNotFoundError: file not found + """ + log = OutputFileHelper.GetLogFilePath(self.output_dir) + + if not file_name.endswith(".struxml") and not file_name.endswith(".str"): + raise ValueError(f"File {file_name} must have extension .struxml or .str") + if not os.path.exists(file_name): + raise FileNotFoundError(f"File {file_name} not found") + + cmd_open = CmdOpen(file_name) + fdscript = Fdscript(log, [cmd_open]) + self.RunScript(fdscript, "open") + + def SetVerbosity(self, verbosity : Verbosity): + super().LogLevel(verbosity.value) + + def GenerateListTables(self, bsc_file : str, csv_file : str = None): + log = OutputFileHelper.GetLogFilePath(self.output_dir) + + cmd_results = CmdListGen(bsc_file, csv_file) + fdscript = Fdscript(log, [cmd_results]) + self.RunScript(fdscript, "generate_list_tables") + + + ## it does not work + def Disconnect(self): + super().Detach() + win32pipe.DisconnectNamedPipe(self.pipe_send) diff --git a/FemDesign.Python/packaging/src/femdesign/database.py b/FemDesign.Python/packaging/src/femdesign/database.py new file mode 100644 index 00000000..8c9384e3 --- /dev/null +++ b/FemDesign.Python/packaging/src/femdesign/database.py @@ -0,0 +1,63 @@ +import xml.etree.ElementTree as ET +import uuid +import datetime + +namespace = {'': 'urn:strusoft'} + +class Database: + def __init__(self, country): + self.struxml_version = "01.00.000" + self.source_software = f"FEM-Design API SDK {self.get_version()}" + self.start_time = "1970-01-01T00:00:00.000" + self.end_time = datetime.datetime.now(datetime.UTC).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + self.guid = str(uuid.uuid4()) + self.convert_id = "00000000-0000-0000-0000-000000000000" + self.standard = "EC" + self.country = country + self.end = "" + + def get_version(self): + return "0.1.0" + + @property + def eurocode(self): + return self._root.attrib["standard"] + + @property + def country(self): + return self._root.attrib["country"] + + @property + def source_software(self): + return self._root.attrib["source_software"] + + @property + def entities(self): + return self._root.findall(".//entities", namespace) + + @property + def sections(self): + return self._root.findall(".//sections", namespace) + + @property + def materials(self): + return self._root.findall(".//materials", namespace) + + @property + def bars(self): + return self._root.findall(".//bar", namespace) + + def serialise_to_xml(self): + return ET.tostring(self._root, encoding="UTF-8") + + # private void Initialize(Country country) + # { + # this.StruxmlVersion = "01.00.000"; + # this.SourceSoftware = $"FEM-Design API SDK {Assembly.GetExecutingAssembly().GetName().Version.ToString()}"; + # this.StartTime = "1970-01-01T00:00:00.000"; + # this.EndTime = System.DateTime.UtcNow.ToString("yyyy-MM-ddTHH:mm:ss.fff", CultureInfo.InvariantCulture); + # this.Guid = System.Guid.NewGuid(); + # this.ConvertId = "00000000-0000-0000-0000-000000000000"; + # this.Standard = "EC"; + # this.Country = country; + # this.End = ""; \ No newline at end of file diff --git a/FemDesign.Python/packaging/src/femdesign/utilities/__init__.py b/FemDesign.Python/packaging/src/femdesign/utilities/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/FemDesign.Python/packaging/src/femdesign/utilities/filehelper.py b/FemDesign.Python/packaging/src/femdesign/utilities/filehelper.py new file mode 100644 index 00000000..91e1eaf9 --- /dev/null +++ b/FemDesign.Python/packaging/src/femdesign/utilities/filehelper.py @@ -0,0 +1,40 @@ +import os +import pathlib +## define a static class to help with file operations +class OutputFileHelper: + _scriptsDirectory = "scripts" + _resultsDirectory = "results" + _bscDirectory = "bsc" + + _logFileName = "logfile.log" + _struxmlFileName = "model.struxml" + _strFileName = "model.str" + _docxFileName = "model.docx" + + _fdscriptFileExtension = ".fdscript" + _bscFileExtension = ".bsc" + _csvFileExtension = ".csv" + + _strFileExtension = ".str" + _struxmlFileExtension = ".struxml" + + def GetLogFilePath(outputDirectory: str) -> str: + if not os.path.exists(outputDirectory): + os.makedirs(outputDirectory) + return os.path.abspath( os.path.join(outputDirectory, OutputFileHelper._logFileName) ) + + def GetFdscriptFilePath(outputDirectory: str, file_name: str = "script") -> str: + dir = os.path.join(outputDirectory, OutputFileHelper._scriptsDirectory) + if not os.path.exists(dir): + os.makedirs(dir) + + path = os.path.abspath( os.path.join(dir, f"{file_name}" + OutputFileHelper._fdscriptFileExtension) ) + return path + + def GetBscFilePath(outputDirectory: str, file_name: str) -> str: + dir = os.path.join(outputDirectory, OutputFileHelper._scriptsDirectory, OutputFileHelper._bscDirectory) + if not os.path.exists(dir): + os.makedirs(dir) + + path = os.path.abspath( os.path.join(dir, f"{file_name}" + OutputFileHelper._bscFileExtension) ) + return path \ No newline at end of file diff --git a/FemDesign.Python/test/test_analysis.py b/FemDesign.Python/test/test_analysis.py new file mode 100644 index 00000000..80612096 --- /dev/null +++ b/FemDesign.Python/test/test_analysis.py @@ -0,0 +1,29 @@ +from command import * +from analysis import Analysis, Comb, Design + +def test_design(): + xmlDesign = Design(True, True, True).to_xml_element() + + assert xmlDesign.tag == "design" + assert xmlDesign.attrib == {} + assert xmlDesign.text == None + + assert xmlDesign.find("autodesign") != None + assert xmlDesign.find("autodesign").text == "true" + assert xmlDesign.find("check").text == "true" + assert xmlDesign.find("gmax") == None + assert xmlDesign.find("cmax") != None + + + xmlDesign = Design(False, False, False).to_xml_element() + + assert xmlDesign.tag == "design" + assert xmlDesign.attrib == {} + assert xmlDesign.text == None + + assert xmlDesign.find("autodesign") != None + assert xmlDesign.find("autodesign").text == "false" + assert xmlDesign.find("check").text == "false" + + assert xmlDesign.find("gmax") != None + assert xmlDesign.find("cmax") == None diff --git a/FemDesign.Python/test/test_command.py b/FemDesign.Python/test/test_command.py new file mode 100644 index 00000000..34f38913 --- /dev/null +++ b/FemDesign.Python/test/test_command.py @@ -0,0 +1,130 @@ +from command import * +from analysis import Analysis, Comb + +def test_cmd_open(): + file_path = "myFilePath.str" + xmlCmdOpen = CmdOpen(file_path).to_xml_element() + + assert xmlCmdOpen.tag == "cmdopen" + assert xmlCmdOpen.attrib.get("command") == "; CXL CS2SHELL OPEN" + + filename = xmlCmdOpen.find("filename") + assert filename.text == os.path.join( os.getcwd(), file_path ) + +def test_cmd_save(): + file_path = "myFilePath.str" + xmlCmdSave = CmdSave(file_path).to_xml_element() + + assert xmlCmdSave.tag == "cmdsave" + assert xmlCmdSave.attrib.get("command") == "; CXL CS2SHELL SAVE" + + filename = xmlCmdSave.find("filename") + assert filename.text == os.path.join( os.getcwd(), file_path ) + +def test_cmd_user(): + for user in User: + xmlCmdUser = CmdUser(user).to_xml_element() + + assert xmlCmdUser.attrib.get("command") == f"; CXL $MODULE {user.name}" + assert xmlCmdUser.tag == "cmduser" + +def test_cmd_end_session(): + xmlCmdEndSession = CmdEndSession().to_xml_element() + + assert xmlCmdEndSession.tag == "cmdendsession" + assert xmlCmdEndSession.text == None + assert xmlCmdEndSession.attrib == {} + +def test_analysis(): + xmlAnalysis = Analysis.StaticAnalysis().to_xml_element() + + assert xmlAnalysis.tag == "analysis" + + assert xmlAnalysis.attrib.get("calcCase") == "1" + assert xmlAnalysis.attrib.get("calcComb") == "1" + assert xmlAnalysis.find("Comb") is None + assert xmlAnalysis.attrib.get("calcStab") == "0" + + xmlAnalysis = Analysis.StaticAnalysis(Comb.Default(), True).to_xml_element() + assert xmlAnalysis.attrib.get("calcCase") == "1" + assert xmlAnalysis.attrib.get("calcComb") == "1" + assert xmlAnalysis.find("comb") is not None + + xmlAnalysis = Analysis.StaticAnalysis(Comb.Default(), False).to_xml_element() + assert xmlAnalysis.attrib.get("calcCase") == "1" + assert xmlAnalysis.attrib.get("calcComb") == "0" + assert xmlAnalysis.find("comb") is not None + + xmlAnalysis = Analysis.FrequencyAnalysis().to_xml_element() + assert xmlAnalysis.attrib.get("calcFreq") == "1" + assert xmlAnalysis.find("comb") is None + assert xmlAnalysis.find("freq") is not None + +def test_cmd_proj_descr(): + xmlCmdProjDescr = CmdProjDescr("Test project", "Test project description", "Test designer", "Test signature", "Comment", None).to_xml_element() + + xmlCmdProjDescr.tag == "cmdprojdescr" + + assert xmlCmdProjDescr.attrib.get("szProject") == "Test project" + assert xmlCmdProjDescr.attrib.get("szDescription") == "Test project description" + assert xmlCmdProjDescr.attrib.get("szDesigner") == "Test designer" + assert xmlCmdProjDescr.attrib.get("szSignature") == "Test signature" + assert xmlCmdProjDescr.attrib.get("szComment") == "Comment" + + assert xmlCmdProjDescr.attrib.get("read") == "0" + assert xmlCmdProjDescr.attrib.get("reset") == "0" + + assert xmlCmdProjDescr.find("item") is None + + items = {"a": "a_txt", "b": "b_txt"} + xmlCmdProjDescr = CmdProjDescr(None, None, None, None, None, items, 0, 0).to_xml_element() + + xmlCmdProjDescr.tag == "cmdprojdescr" + + assert xmlCmdProjDescr.attrib.get("szProject") == None + assert xmlCmdProjDescr.attrib.get("szDescription") == None + assert xmlCmdProjDescr.attrib.get("szDesigner") == None + assert xmlCmdProjDescr.attrib.get("szSignature") == None + assert xmlCmdProjDescr.attrib.get("szComment") == None + + assert xmlCmdProjDescr.attrib.get("read") == "0" + assert xmlCmdProjDescr.attrib.get("reset") == "0" + + assert xmlCmdProjDescr.find("item") is not None + assert len( xmlCmdProjDescr.findall("item") ) == 2 + +def test_cmd_listgen(): + xmlCmdListGen = CmdListGen("bscfile.bsc", "outfile.csv", None, True, True, True).to_xml_element() + + assert xmlCmdListGen.tag == "cmdlistgen" + assert xmlCmdListGen.attrib.get("bscfile") == os.path.join( os.getcwd(), "bscfile.bsc" ) + assert xmlCmdListGen.attrib.get("outfile") == os.path.join( os.getcwd(), "outfile.csv" ) + assert xmlCmdListGen.attrib.get("regional") == "1" + assert xmlCmdListGen.attrib.get("fillcells") == "1" + assert xmlCmdListGen.attrib.get("headers") == "1" + assert xmlCmdListGen.find("GUID") is None + assert len( xmlCmdListGen.findall("GUID") ) == 0 + + + guids = [uuid.uuid4(), uuid.uuid4()] + xmlCmdListGen = CmdListGen("result.bsc", "outfile.csv", guids, False, False, False).to_xml_element() + + assert xmlCmdListGen.tag == "cmdlistgen" + assert xmlCmdListGen.attrib.get("bscfile") == os.path.join( os.getcwd(), "result.bsc" ) + assert xmlCmdListGen.attrib.get("outfile") == os.path.join( os.getcwd(), "outfile.csv" ) + assert xmlCmdListGen.attrib.get("regional") == "0" + assert xmlCmdListGen.attrib.get("fillcells") == "0" + assert xmlCmdListGen.attrib.get("headers") == "0" + assert xmlCmdListGen.find("GUID") is not None + assert len( xmlCmdListGen.findall("GUID") ) == 2 + + xmlCmdListGen = CmdListGen("result.bsc").to_xml_element() + + assert xmlCmdListGen.tag == "cmdlistgen" + assert xmlCmdListGen.attrib.get("bscfile") == os.path.join( os.getcwd(), "result.bsc" ) + assert xmlCmdListGen.attrib.get("outfile") == os.path.join( os.getcwd(), "result.csv" ) + assert xmlCmdListGen.attrib.get("regional") == "1" + assert xmlCmdListGen.attrib.get("fillcells") == "1" + assert xmlCmdListGen.attrib.get("headers") == "1" + assert xmlCmdListGen.find("GUID") is None + assert len( xmlCmdListGen.findall("GUID") ) == 0 diff --git a/FemDesign.Python/test/test_pipe.py b/FemDesign.Python/test/test_pipe.py new file mode 100644 index 00000000..541250b9 --- /dev/null +++ b/FemDesign.Python/test/test_pipe.py @@ -0,0 +1,28 @@ +from command import * +from analysis import Analysis, Comb, Design +from fdpipe import FemDesignConnection +import pytest + +def test_pipe(): + connection = FemDesignConnection(output_dir="test", minimized=True) + assert connection.output_dir == os.path.join( os.getcwd(), "test" ) + + connection._output_dir = None + assert connection.output_dir == os.path.join( os.getcwd(), "FEM-Design API" ) + + ## assert that connection.open() raises an error + try: + connection.Open("myModel.str") + except Exception as e: + assert isinstance(e, FileNotFoundError) + assert str(e) == "File myModel.str not found" + + try: + connection.Open("myModel.3dm") + except Exception as e: + assert isinstance(e, ValueError) + assert str(e) == "File myModel.3dm must have extension .struxml or .str" + + + connection.RunDesign(DesignModule.STEELDESIGN, design) +