From 59bd16c1ff316ecc3c9a2eba6e92bc98bd0572c7 Mon Sep 17 00:00:00 2001 From: Chris McComb Date: Fri, 29 Dec 2023 16:14:05 -0500 Subject: [PATCH] Goals are now handled separately from the core truss functionality and integrated only for reporting --- tests/test_construction_and_io.py | 58 ++++++++------- tests/test_custom_materials_and_shapes.py | 21 +++++- trussme/__init__.py | 2 +- trussme/report.py | 90 +++++++++++++---------- trussme/truss.py | 78 +++++--------------- 5 files changed, 118 insertions(+), 131 deletions(-) diff --git a/tests/test_construction_and_io.py b/tests/test_construction_and_io.py index e54d0ca..6db7b91 100644 --- a/tests/test_construction_and_io.py +++ b/tests/test_construction_and_io.py @@ -10,6 +10,14 @@ class TestSequenceFunctions(unittest.TestCase): def test_build_methods(self): + goals = trussme.Goals( + minimum_fos_buckling=1.5, + minimum_fos_total=1.5, + minimum_fos_yielding=1.5, + maximum_mass=5.0, + maximum_deflection=6e-3, + ) + # Build truss from scratch t1 = trussme.Truss() t1.add_pinned_support([0.0, 0.0, 0.0]) @@ -51,21 +59,12 @@ def test_build_methods(self): t1.add_member(4, 10) t1.add_member(10, 5) - t1.minimum_fos_buckling = 1.5 - t1.min_fos_yielding = 1.5 - t1.max_mass = 5.0 - t1.maximum_deflection = 6e-3 - # Build truss from file t2 = trussme.read_trs(TEST_TRUSS_FILENAME) - t2.minimum_fos_buckling = 1.5 - t2.min_fos_yielding = 1.5 - t2.max_mass = 5.0 - t2.maximum_deflection = 6e-3 # Save reports - t1.report_to_md(os.path.join(os.path.dirname(__file__), "report_1.md")) - t2.report_to_md(os.path.join(os.path.dirname(__file__), "report_2.md")) + t1.report_to_md(os.path.join(os.path.dirname(__file__), "report_1.md"), goals) + t2.report_to_md(os.path.join(os.path.dirname(__file__), "report_2.md"), goals) # Test for sameness file_are_the_same = filecmp.cmp( @@ -79,25 +78,24 @@ def test_build_methods(self): os.remove(os.path.join(os.path.dirname(__file__), "report_2.md")) def test_save_to_trs_and_rebuild(self): + goals = trussme.Goals( + minimum_fos_buckling=1.5, + minimum_fos_total=1.5, + minimum_fos_yielding=1.5, + maximum_mass=5.0, + maximum_deflection=6e-3, + ) + # Build truss from file t2 = trussme.read_trs(TEST_TRUSS_FILENAME) - t2.minimum_fos_buckling = 1.5 - t2.min_fos_yielding = 1.5 - t2.max_mass = 5.0 - t2.maximum_deflection = 6e-3 # Save - t2.report_to_md(os.path.join(os.path.dirname(__file__), "report_2.md")) + t2.report_to_md(os.path.join(os.path.dirname(__file__), "report_2.md"), goals) t2.to_trs(os.path.join(os.path.dirname(__file__), "asdf.trs")) # Rebuild t3 = trussme.read_trs(os.path.join(os.path.dirname(__file__), "asdf.trs")) - t3.minimum_fos_buckling = 1.5 - t3.min_fos_yielding = 1.5 - t3.max_mass = 5.0 - t3.maximum_deflection = 6e-3 - - t3.report_to_md(os.path.join(os.path.dirname(__file__), "report_3.md")) + t3.report_to_md(os.path.join(os.path.dirname(__file__), "report_3.md"), goals) with open(os.path.join(os.path.dirname(__file__), "report_3.md")) as f: print(f.read()) @@ -112,15 +110,19 @@ def test_save_to_trs_and_rebuild(self): self.assertTrue(file_are_the_same) def test_save_to_json_and_rebuild(self): + goals = trussme.Goals( + minimum_fos_buckling=1.5, + minimum_fos_total=1.5, + minimum_fos_yielding=1.5, + maximum_mass=5.0, + maximum_deflection=6e-3, + ) + # Build truss from file t2 = trussme.read_trs(TEST_TRUSS_FILENAME) - t2.minimum_fos_buckling = 1.5 - t2.min_fos_yielding = 1.5 - t2.max_mass = 5.0 - t2.maximum_deflection = 6e-3 # Save - t2.report_to_md(os.path.join(os.path.dirname(__file__), "report_4.md")) + t2.report_to_md(os.path.join(os.path.dirname(__file__), "report_4.md"), goals) t2.to_trs(os.path.join(os.path.dirname(__file__), "asdf.json")) # Rebuild @@ -130,7 +132,7 @@ def test_save_to_json_and_rebuild(self): t3.max_mass = 5.0 t3.maximum_deflection = 6e-3 - t3.report_to_md(os.path.join(os.path.dirname(__file__), "report_5.md")) + t3.report_to_md(os.path.join(os.path.dirname(__file__), "report_5.md"), goals) with open(os.path.join(os.path.dirname(__file__), "report_5.md")) as f: print(f.read()) diff --git a/tests/test_custom_materials_and_shapes.py b/tests/test_custom_materials_and_shapes.py index b28f90d..464cfaf 100644 --- a/tests/test_custom_materials_and_shapes.py +++ b/tests/test_custom_materials_and_shapes.py @@ -24,7 +24,15 @@ def test_custom_material(self): truss.add_member(1, 2, material=unobtanium) truss.add_member(2, 0, material=unobtanium) - print(truss.report) + goals = trussme.Goals( + minimum_fos_buckling=1.5, + minimum_fos_total=1.5, + minimum_fos_yielding=1.5, + maximum_mass=5.0, + maximum_deflection=6e-3, + ) + + print(truss.report(goals)) def test_custom_shape(self): # Build truss from scratch @@ -36,7 +44,6 @@ def test_custom_shape(self): truss.joints[1].loads[1] = -20000 class MagicalRod(trussme.Shape): - def __init__(self): self.h = None self.w = None @@ -56,4 +63,12 @@ def name(self) -> str: truss.add_member(1, 2, shape=MagicalRod()) truss.add_member(2, 0, shape=MagicalRod()) - print(truss.report) \ No newline at end of file + goals = trussme.Goals( + minimum_fos_buckling=1.5, + minimum_fos_total=1.5, + minimum_fos_yielding=1.5, + maximum_mass=5.0, + maximum_deflection=6e-3, + ) + + print(truss.report(goals)) diff --git a/trussme/__init__.py b/trussme/__init__.py index c004f23..d7afd05 100644 --- a/trussme/__init__.py +++ b/trussme/__init__.py @@ -9,4 +9,4 @@ """ from .components import material_library, Shape, Material, Box, Pipe, Bar, Square -from .truss import Truss, read_trs +from .truss import Truss, read_trs, read_json, Goals diff --git a/trussme/report.py b/trussme/report.py index 3cf1285..de85036 100644 --- a/trussme/report.py +++ b/trussme/report.py @@ -5,7 +5,7 @@ import trussme.visualize -def generate_summary(truss) -> str: +def generate_summary(truss, goals) -> str: """ Generate a summary of the analysis. @@ -13,6 +13,8 @@ def generate_summary(truss) -> str: ---------- truss: Truss The truss to be summarized + goals: Goals + The goals against which to evaluate the truss Returns ------- @@ -31,27 +33,27 @@ def generate_summary(truss) -> str: success_string = [] failure_string = [] - if truss.minimum_fos_total < truss.fos_total: + if goals.minimum_fos_total < truss.fos_total: success_string.append("total FOS") else: failure_string.append("total FOS") - if truss.minimum_fos_buckling < truss.fos_buckling: + if goals.minimum_fos_buckling < truss.fos_buckling: success_string.append("buckling FOS") else: failure_string.append("buckling FOS") - if truss.minimum_fos_yielding < truss.fos_yielding: + if goals.minimum_fos_yielding < truss.fos_yielding: success_string.append("yielding FOS") else: failure_string.append("yielding FOS") - if truss.maximum_mass > truss.mass: + if goals.maximum_mass > truss.mass: success_string.append("mass") else: failure_string.append("mass") - if truss.maximum_deflection > truss.deflection: + if goals.maximum_deflection > truss.deflection: success_string.append("deflection") else: failure_string.append("deflection") @@ -106,37 +108,37 @@ def generate_summary(truss) -> str: ] data.append( [ - truss.minimum_fos_total, + goals.minimum_fos_total, truss.fos_total, - "Yes" if truss.fos_total > truss.minimum_fos_total else "No", + "Yes" if truss.fos_total > goals.minimum_fos_total else "No", ] ) data.append( [ - truss.minimum_fos_buckling, + goals.minimum_fos_buckling, truss.fos_buckling, - "Yes" if truss.fos_buckling > truss.minimum_fos_buckling else "No", + "Yes" if truss.fos_buckling > goals.minimum_fos_buckling else "No", ] ) data.append( [ - truss.minimum_fos_yielding, + goals.minimum_fos_yielding, truss.fos_yielding, - "Yes" if truss.fos_yielding > truss.minimum_fos_yielding else "No", + "Yes" if truss.fos_yielding > goals.minimum_fos_yielding else "No", ] ) data.append( [ - truss.maximum_mass, + goals.maximum_mass, truss.mass, - "Yes" if truss.mass < truss.maximum_mass else "No", + "Yes" if truss.mass < goals.maximum_mass else "No", ] ) data.append( [ - truss.maximum_deflection, + goals.maximum_deflection, truss.deflection, - "Yes" if truss.deflection < truss.maximum_deflection else "No", + "Yes" if truss.deflection < goals.maximum_deflection else "No", ] ) @@ -152,24 +154,29 @@ def generate_summary(truss) -> str: return summary -def generate_instantiation_information(the_truss) -> str: +def generate_instantiation_information(truss) -> str: """ Generate a summary of the instantiation information. - :param the_truss: The truss to be reported on - :type the_truss: Truss - :return: A report of the instantiation information - :rtype: str + Parameters + ---------- + truss: Truss + The truss to be reported on + + Returns + ------- + str + A report of the instantiation information """ instantiation = "# INSTANTIATION INFORMATION\n" - instantiation += trussme.visualize.plot_truss(the_truss) + "\n" + instantiation += trussme.visualize.plot_truss(truss) + "\n" # Print joint information instantiation += "## JOINTS\n" data = [] rows = [] - for j in the_truss.joints: + for j in truss.joints: rows.append("Joint_" + "{0:02d}".format(j.idx)) data.append( [ @@ -192,7 +199,7 @@ def generate_instantiation_information(the_truss) -> str: instantiation += "\n## MEMBERS\n" data = [] rows = [] - for m in the_truss.members: + for m in truss.members: rows.append("Member_" + "{0:02d}".format(m.idx)) data.append( [ @@ -228,7 +235,7 @@ def generate_instantiation_information(the_truss) -> str: instantiation += "\n## MATERIALS\n" data = [] rows = [] - for mat in the_truss.materials: + for mat in truss.materials: rows.append(mat["name"]) data.append( [ @@ -251,14 +258,21 @@ def generate_instantiation_information(the_truss) -> str: return instantiation -def generate_stress_analysis(the_truss) -> str: +def generate_stress_analysis(truss, goals) -> str: """ Generate a summary of the stress analysis information. - :param the_truss: The truss to be reported on - :type the_truss: Truss - :return: A report of the stress analysis information - :rtype: str + Parameters + ---------- + truss: Truss + The truss to be reported on + goals: Goals + The goals against which to evaluate the truss + + Returns + ------- + str + A report of the stress analysis information """ analysis = "# STRESS ANALYSIS INFORMATION\n" @@ -266,7 +280,7 @@ def generate_stress_analysis(the_truss) -> str: analysis += "## LOADING\n" data = [] rows = [] - for j in the_truss.joints: + for j in truss.joints: rows.append("Joint_" + "{0:02d}".format(j.idx)) data.append( [ @@ -288,7 +302,7 @@ def generate_stress_analysis(the_truss) -> str: analysis += "\n## REACTIONS\n" data = [] rows = [] - for j in the_truss.joints: + for j in truss.joints: rows.append("Joint_" + "{0:02d}".format(j.idx)) data.append( [ @@ -314,7 +328,7 @@ def generate_stress_analysis(the_truss) -> str: analysis += "\n## FORCES AND STRESSES\n" data = [] rows = [] - for m in the_truss.members: + for m in truss.members: rows.append("Member_" + "{0:02d}".format(m.idx)) data.append( [ @@ -322,10 +336,10 @@ def generate_stress_analysis(the_truss) -> str: format(m.moment_of_inertia, ".2e"), format(m.force / pow(10, 3), ".2f"), m.fos_yielding, - "Yes" if m.fos_yielding > the_truss.minimum_fos_yielding else "No", + "Yes" if m.fos_yielding > goals.minimum_fos_yielding else "No", m.fos_buckling if m.fos_buckling > 0 else "N/A", "Yes" - if m.fos_buckling > the_truss.minimum_fos_buckling or m.fos_buckling < 0 + if m.fos_buckling > goals.minimum_fos_buckling or m.fos_buckling < 0 else "No", ] ) @@ -347,11 +361,11 @@ def generate_stress_analysis(the_truss) -> str: # Print information about members analysis += "\n## DEFLECTIONS\n" - analysis += trussme.visualize.plot_truss(the_truss, deflected_shape=True) + "\n" + analysis += trussme.visualize.plot_truss(truss, deflected_shape=True) + "\n" data = [] rows = [] - for j in the_truss.joints: + for j in truss.joints: rows.append("Joint_" + "{0:02d}".format(j.idx)) data.append( [ @@ -365,7 +379,7 @@ def generate_stress_analysis(the_truss) -> str: if j.translation[2] == 0.0 else "N/A", "Yes" - if numpy.linalg.norm(j.deflections) < the_truss.maximum_deflection + if numpy.linalg.norm(j.deflections) < goals.maximum_deflection else "No", ] ) diff --git a/trussme/truss.py b/trussme/truss.py index 3181498..f09a90d 100644 --- a/trussme/truss.py +++ b/trussme/truss.py @@ -27,24 +27,24 @@ class Goals: Attributes ---------- - min_fos_total: float, default=1.0 + minimum_fos_total: float, default=1.0 Minimum total FOS for the truss, defaults to 1.0 - min_fos_buckling: float, default=1.0 + minimum_fos_buckling: float, default=1.0 Minimum buckling FOS for the truss, defaults to 1.0 - min_fos_yielding: float, default=1.0 + minimum_fos_yielding: float, default=1.0 Minimum yielding FOS for the truss, defaults to 1.0 - max_mass: float, default=inf + maximum_mass: float, default=inf Maximum mass for the truss, defaults to inf - max_deflection: float, default=inf + maximum_deflection: float, default=inf Maximum deflection for the truss, defaults to inf """ - min_fos_total: float = 1.0 - min_fos_buckling: float = 1.0 - min_fos_yielding: float = 1.0 - max_mass: float = numpy.inf - max_deflection: float = numpy.inf + minimum_fos_total: float = 1.0 + minimum_fos_buckling: float = 1.0 + minimum_fos_yielding: float = 1.0 + maximum_mass: float = numpy.inf + maximum_deflection: float = numpy.inf class Truss(object): @@ -56,8 +56,6 @@ class Truss(object): A list of all members in the truss joints: list[Joint] A list of all joints in the truss - goals: Goals - A container of goals for truss design """ def __init__(self): @@ -67,9 +65,6 @@ def __init__(self): # Make a list to store joints in self.joints: list[Joint] = [] - # Design goals - self.goals: Goals = Goals() - @property def number_of_members(self) -> int: """int: Number of members in the truss""" @@ -124,46 +119,6 @@ def limit_state(self) -> Literal["buckling", "yielding"]: else: return "yielding" - @property - def minimum_fos_total(self) -> float: - return self.goals.min_fos_total - - @minimum_fos_total.setter - def minimum_fos_total(self, new_fos: float): - self.goals.min_fos_total = new_fos - - @property - def minimum_fos_yielding(self) -> float: - return self.goals.min_fos_yielding - - @minimum_fos_yielding.setter - def minimum_fos_yielding(self, new_fos: float): - self.goals.min_fos_yielding = new_fos - - @property - def minimum_fos_buckling(self) -> float: - return self.goals.min_fos_buckling - - @minimum_fos_buckling.setter - def minimum_fos_buckling(self, new_fos: float): - self.goals.min_fos_buckling = new_fos - - @property - def maximum_mass(self) -> float: - return self.goals.max_mass - - @maximum_mass.setter - def maximum_mass(self, new_mass: float): - self.goals.max_mass = new_mass - - @property - def maximum_deflection(self) -> float: - return self.goals.max_deflection - - @maximum_deflection.setter - def maximum_deflection(self, new_deflection: float): - self.goals.max_deflection = new_deflection - def add_pinned_support(self, coordinates: list[float]): # Make the joint self.joints.append(Joint(coordinates)) @@ -256,18 +211,17 @@ def calc_fos(self): + ". Results may be inaccurate." ) - @property - def report(self) -> str: + def report(self, goals: Goals) -> str: """str: A full report on the truss""" self.calc_fos() - report_string = report.generate_summary(self) + "\n" + report_string = report.generate_summary(self, goals) + "\n" report_string += report.generate_instantiation_information(self) + "\n" - report_string += report.generate_stress_analysis(self) + "\n" + report_string += report.generate_stress_analysis(self, goals) + "\n" return report_string - def report_to_md(self, file_name: str) -> None: + def report_to_md(self, file_name: str, goals: Goals) -> None: """ Writes a report in Markdown format @@ -275,13 +229,15 @@ def report_to_md(self, file_name: str) -> None: ---------- file_name: str The name of the file + goals: Goals + Goals against which to evaluate the truss Returns ------- None """ with open(file_name, "w") as f: - f.write(self.report) + f.write(self.report(goals)) def to_json(self, file_name: str) -> None: """