From 7e97c5aac968c8f3201b229164b621ad34e37ac6 Mon Sep 17 00:00:00 2001 From: Chris McComb Date: Sun, 7 Jan 2024 11:33:19 -0500 Subject: [PATCH] Cleanup --- tests/test_optimization.py | 83 +++++++++++++++++++++++++-- trussme/optimize.py | 112 ++++++++++++++++++++++++++++++++++++- 2 files changed, 187 insertions(+), 8 deletions(-) diff --git a/tests/test_optimization.py b/tests/test_optimization.py index c30fd34..e59fb87 100644 --- a/tests/test_optimization.py +++ b/tests/test_optimization.py @@ -51,7 +51,7 @@ def test_setup(self): goals = trussme.Goals() - x0, obj, con, gen = trussme.make_optimization_functions( + x0, obj, con, gen, bnds = trussme.make_optimization_functions( truss_from_commands, goals ) @@ -103,16 +103,87 @@ def test_joint_optimization(self): goals = trussme.Goals() - x0, obj, con, gen = trussme.make_optimization_functions( + x0, obj, con, gen, bnds = trussme.make_optimization_functions( truss_from_commands, goals, joint_coordinates=True, shape_parameters=False ) results = scipy.optimize.minimize( - lambda x, *args: obj(x), + obj, x0, - scipy.optimize.NonlinearConstraint( + constraints=[ + scipy.optimize.NonlinearConstraint( + con, -numpy.inf, 0.0, keep_feasible=True + ) + ], + method="trust-constr", + options={"verbose": 2, "maxiter": 50}, + ) + + result_truss = gen(results.x) + result_truss.analyze() + trussme.report_to_md( + os.path.join(os.path.dirname(__file__), "joint_optim.md"), + result_truss, + goals, + ) + + def test_full_optimization(self): + truss_from_commands = trussme.Truss() + truss_from_commands.add_pinned_joint([0.0, 0.0, 0.0]) + truss_from_commands.add_free_joint([1.0, 0.0, 0.0]) + truss_from_commands.add_free_joint([2.0, 0.0, 0.0]) + truss_from_commands.add_free_joint([3.0, 0.0, 0.0]) + truss_from_commands.add_free_joint([4.0, 0.0, 0.0]) + truss_from_commands.add_pinned_joint([5.0, 0.0, 0.0]) + + truss_from_commands.add_free_joint([0.5, 1.0, 0.0]) + truss_from_commands.add_free_joint([1.5, 1.0, 0.0]) + truss_from_commands.add_free_joint([2.5, 1.0, 0.0]) + truss_from_commands.add_free_joint([3.5, 1.0, 0.0]) + truss_from_commands.add_free_joint([4.5, 1.0, 0.0]) + + truss_from_commands.add_out_of_plane_support("z") + + truss_from_commands.joints[8].loads[1] = -20000 + + truss_from_commands.add_member(0, 1) + truss_from_commands.add_member(1, 2) + truss_from_commands.add_member(2, 3) + truss_from_commands.add_member(3, 4) + truss_from_commands.add_member(4, 5) + + truss_from_commands.add_member(6, 7) + truss_from_commands.add_member(7, 8) + truss_from_commands.add_member(8, 9) + truss_from_commands.add_member(9, 10) + + truss_from_commands.add_member(0, 6) + truss_from_commands.add_member(6, 1) + truss_from_commands.add_member(1, 7) + truss_from_commands.add_member(7, 2) + truss_from_commands.add_member(2, 8) + truss_from_commands.add_member(8, 3) + truss_from_commands.add_member(3, 9) + truss_from_commands.add_member(9, 4) + truss_from_commands.add_member(4, 10) + truss_from_commands.add_member(10, 5) + + goals = trussme.Goals() + + x0, obj, con, gen, bnds = trussme.make_optimization_functions( + truss_from_commands, + goals, + joint_coordinates=True, + shape_parameters=True, + ) + + results = scipy.optimize.minimize( + obj, + x0, + constraints=scipy.optimize.NonlinearConstraint( con, -numpy.inf, 0.0, keep_feasible=True ), + bounds=scipy.optimize.Bounds(bnds[0], bnds[1], keep_feasible=True), method="trust-constr", options={"verbose": 2, "maxiter": 50}, ) @@ -120,5 +191,7 @@ def test_joint_optimization(self): result_truss = gen(results.x) result_truss.analyze() trussme.report_to_md( - os.path.join(os.path.dirname(__file__), "optim.md"), result_truss, goals + os.path.join(os.path.dirname(__file__), "full_optim.md"), + result_truss, + goals, ) diff --git a/trussme/optimize.py b/trussme/optimize.py index cc54d54..f8b37fc 100644 --- a/trussme/optimize.py +++ b/trussme/optimize.py @@ -1,9 +1,8 @@ from typing import Callable -import io import numpy -from trussme import Truss, Goals, read_json +from trussme import Truss, Goals, read_json, Pipe, Box, Square, Bar def make_x0( @@ -48,9 +47,86 @@ def make_x0( if planar_direction != "z": x0.append(configured_truss.joints[i].coordinates[2]) + if shape_parameters: + for i in range(len(configured_truss.members)): + shape_name: str = configured_truss.members[i].shape.name() + if shape_name == "pipe": + x0.append(configured_truss.members[i].shape._params["r"]) + elif shape_name == "box": + x0.append(configured_truss.members[i].shape._params["w"]) + elif shape_name == "bar": + x0.append(configured_truss.members[i].shape._params["r"]) + elif shape_name == "square": + x0.append(configured_truss.members[i].shape._params["w"]) + return x0 +def make_bounds( + truss: Truss, + joint_coordinates: bool = True, + shape_parameters: bool = True, +) -> tuple[list[float], list[float]]: + """ + Returns a vector that encodes the current truss design + + Parameters + ---------- + truss: Truss + The truss to configure. + joint_coordinates: bool, default=True + Whether to include joint location parameters. + shape_parameters: bool, default=True + Whether to include shape parameters. + + Returns + ------- + list[float] + A starting vector that encodes the current truss design + """ + + planar_direction: str = truss.is_planar() + lb: list[float] = [] + ub: list[float] = [] + + configured_truss = read_json(truss.to_json()) + + if joint_coordinates: + for i in range(len(configured_truss.joints)): + if ( + numpy.sum(configured_truss.joints[i].translation_restricted) + == (0 if planar_direction == "none" else 1) + and numpy.sum(configured_truss.joints[i].loads) == 0 + ): + if planar_direction != "x": + lb.append(-numpy.inf) + ub.append(numpy.inf) + if planar_direction != "y": + lb.append(-numpy.inf) + ub.append(numpy.inf) + if planar_direction != "z": + lb.append(-numpy.inf) + ub.append(numpy.inf) + + if shape_parameters: + for i in range(len(configured_truss.members)): + shape_name: str = configured_truss.members[i].shape.name() + if shape_name == "pipe": + lb.append(0.0) + ub.append(numpy.inf) + elif shape_name == "box": + lb.append(0.0) + ub.append(numpy.inf) + elif shape_name == "bar": + lb.append(0.0) + ub.append(numpy.inf) + elif shape_name == "square": + lb.append(0.0) + ub.append(numpy.inf) + + return lb, ub + + def make_truss_generator_function( truss: Truss, joint_coordinates: bool = True, @@ -97,6 +173,29 @@ def truss_generator(x: list[float]) -> Truss: configured_truss.joints[i].coordinates[2] = x[idx] idx += 1 + if shape_parameters: + for i in range(len(configured_truss.members)): + shape_name: str = configured_truss.members[i].shape.name() + p = configured_truss.members[i].shape._params + if shape_name == "pipe": + configured_truss.members[i].shape = Pipe( + r=x[idx], t=x[idx] * p["t"] / p["r"] + ) + idx += 1 + elif shape_name == "box": + configured_truss.members[i].shape = Box( + w=x[idx], h=x[idx] * p["h"] / p["w"], t=x[idx] * p["t"] / p["w"] + ) + idx += 1 + elif shape_name == "bar": + configured_truss.members[i].shape = Bar(r=x[idx]) + idx += 1 + elif shape_name == "square": + configured_truss.members[i].shape = Square( + w=x[idx], h=x[idx] * p["h"] / p["w"] + ) + idx += 1 + return configured_truss return truss_generator @@ -112,6 +211,7 @@ def make_optimization_functions( Callable[[list[float]], float], Callable[[list[float]], list[float]], Callable[[list[float]], Truss], + tuple[list[float], list[float]], ]: """ Creates functions for use in optimization, including a starting vector, objective function, a constraint function, and a truss generator function. @@ -120,6 +220,8 @@ def make_optimization_functions( ---------- truss: Truss The truss to use as a starting configuration + goals: Goals + The goals to use for optimization joint_coordinates: bool, default=True Whether to include joint location parameters. shape_parameters: bool, default=True @@ -134,6 +236,7 @@ def make_optimization_functions( Callable[[list[float]], Truss], ] A tuple containing the starting vector, objective function, constraint function, and truss generator function. + :param goals: """ x0 = make_x0(truss, joint_coordinates, shape_parameters) @@ -152,12 +255,15 @@ def inequality_constraints(x: list[float]) -> list[float]: return [ goals.minimum_fos_buckling - truss.fos_buckling, goals.minimum_fos_yielding - truss.fos_yielding, - truss.deflection - goals.maximum_deflection, + truss.deflection - numpy.min([goals.maximum_deflection, 10000.0]), ] + bounds = make_bounds(truss, joint_coordinates, shape_parameters) + return ( x0, objective_function, inequality_constraints, truss_generator, + bounds, )