morphopt 3.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of morphopt might be problematic. Click here for more details.

Files changed (49) hide show
  1. morphopt/__init__.py +32 -0
  2. morphopt/opt_runner.py +130 -0
  3. morphopt/optcore/baseobject.py +48 -0
  4. morphopt/optcore/controller.py +404 -0
  5. morphopt/optcore/history.py +440 -0
  6. morphopt/optcore/modelparams/__init__.py +4 -0
  7. morphopt/optcore/modelparams/base_params.py +97 -0
  8. morphopt/optcore/modelparams/feamodel/__init__.py +0 -0
  9. morphopt/optcore/modelparams/feamodel/feainterface/__init__.py +10 -0
  10. morphopt/optcore/modelparams/feamodel/feainterface/basefeainterface.py +50 -0
  11. morphopt/optcore/modelparams/feamodel/feainterface/bodyforceinterface.py +71 -0
  12. morphopt/optcore/modelparams/feamodel/feainterface/boundaryconditioninterface.py +59 -0
  13. morphopt/optcore/modelparams/feamodel/feainterface/contactinterface.py +113 -0
  14. morphopt/optcore/modelparams/feamodel/feainterface/coupleinterface.py +31 -0
  15. morphopt/optcore/modelparams/feamodel/feainterface/pointinterface.py +79 -0
  16. morphopt/optcore/modelparams/feamodel/feainterface/pressureinterface.py +68 -0
  17. morphopt/optcore/modelparams/feamodel/feainterface/referencepointinterface.py +29 -0
  18. morphopt/optcore/modelparams/feamodel/feainterface/springinterface.py +104 -0
  19. morphopt/optcore/modelparams/feamodel/feaparams.py +264 -0
  20. morphopt/optcore/modelparams/geometry/__init__.py +3 -0
  21. morphopt/optcore/modelparams/geometry/geometryinterfaces/__init__.py +1 -0
  22. morphopt/optcore/modelparams/geometry/geometryinterfaces/basesurfaceinterface.py +763 -0
  23. morphopt/optcore/modelparams/geometry/geometryinterfaces/bspsurfaceinterface.py +723 -0
  24. morphopt/optcore/modelparams/geometry/geometryinterfaces/cpgeosurfaceinterface.py +374 -0
  25. morphopt/optcore/modelparams/geometry/geometryparams.py +715 -0
  26. morphopt/optcore/modelparams/materials/__init__.py +1 -0
  27. morphopt/optcore/modelparams/materials/materialparams.py +110 -0
  28. morphopt/optcore/modelparams/params.py +88 -0
  29. morphopt/optcore/objfunc.py +248 -0
  30. morphopt/optcore/solver.py +159 -0
  31. morphopt/optcore/updaters/__init__.py +2 -0
  32. morphopt/optcore/updaters/base_updater.py +110 -0
  33. morphopt/optcore/updaters/geometry/__init__.py +2 -0
  34. morphopt/optcore/updaters/geometry/objectivefuncs/__init__.py +5 -0
  35. morphopt/optcore/updaters/geometry/objectivefuncs/basefuncs.py +117 -0
  36. morphopt/optcore/updaters/geometry/objectivefuncs/boundarys.py +91 -0
  37. morphopt/optcore/updaters/geometry/objectivefuncs/distancesurface.py +123 -0
  38. morphopt/optcore/updaters/geometry/objectivefuncs/shapederivative.py +179 -0
  39. morphopt/optcore/updaters/geometry/objectivefuncs/surfacefairness.py +40 -0
  40. morphopt/optcore/updaters/geometry/update_geometry.py +347 -0
  41. morphopt/optcore/updaters/optimizer.py +177 -0
  42. morphopt/optcore/updaters/updaters.py +82 -0
  43. morphopt/optcore/utils/plot_history_surface.py +239 -0
  44. morphopt/taskoptmization.py +75 -0
  45. morphopt/taskui.py +458 -0
  46. morphopt-3.1.1.dist-info/METADATA +242 -0
  47. morphopt-3.1.1.dist-info/RECORD +49 -0
  48. morphopt-3.1.1.dist-info/WHEEL +5 -0
  49. morphopt-3.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,110 @@
1
+
2
+
3
+ import torch
4
+ from ..modelparams.base_params import BaseParams
5
+ from ..modelparams.params import Params
6
+ from . import optimizer
7
+ from ..baseobject import BaseObject
8
+
9
+ class BaseUpdater(BaseObject):
10
+ """
11
+ Base class for all Updaters.
12
+ """
13
+
14
+ def __init__(self, params: Params, *args, **kwargs):
15
+ """
16
+ Initialize the BaseUpdater.
17
+ """
18
+ self.max_step_iter = 50
19
+ """Maximum number of iterations for the sub-optimization process.
20
+ """
21
+
22
+ self.obj_funcs: dict[str, callable] = {}
23
+ """A dictionary of objective functions to be optimized.
24
+ The keys are the names of the functions, and the values are the functions themselves.
25
+ """
26
+
27
+ self.params_update: BaseParams = None
28
+ """The parameters of the optimization process.
29
+ This should be set to an instance of a subclass of BaseParams.
30
+ """
31
+
32
+ self.params: Params = params
33
+ """The parameters of optimization process.
34
+ This should be set to an instance of the Params class.
35
+ """
36
+
37
+ self.optimizer: optimizer.BaseOpt
38
+ """The optimization algorithm used to update the design variables.
39
+ This should be set to an instance of a subclass of Optimizer.BaseOpt.
40
+ """
41
+
42
+ self.iteration_total = 0
43
+ """
44
+ The total number of iterations performed in the optimization process.
45
+ """
46
+
47
+ self.U_dim: list[int] = []
48
+ """U_dim: The dimensions of the interest for the optimization problem.
49
+ This is a list of integers representing the dimensions of the displacement vector.
50
+ """
51
+
52
+ def reinitialize(self, iter_now: int, sensitivity: list[torch.Tensor]) -> None:
53
+ """
54
+ reInitialize the updater with the current iteration and sensitivity.
55
+ """
56
+ raise NotImplementedError("This method should be overridden by subclasses.")
57
+
58
+ def initialize(self) -> None:
59
+ """
60
+ Initialize the updater.
61
+ """
62
+ raise NotImplementedError("This method should be overridden by subclasses.")
63
+
64
+ def _get_sensitivity(self) -> tuple[torch.Tensor, list[torch.Tensor]]:
65
+ """
66
+ Get the sensitivity of the elements based on the provided objective function.
67
+
68
+ Parameters:
69
+ fe_result (FE_result): The finite element analysis result containing the displacement and Jacobian.
70
+ obj_fun (callable): The objective function to be used for sensitivity analysis.
71
+
72
+ Returns:
73
+ tuple: A tuple containing:
74
+ - Loss (torch.Tensor): The loss value.
75
+ - sensitivity (list[torch.Tensor]): A list of sensitivity tensors for each surface.
76
+ """
77
+ raise NotImplementedError("This method should be overridden by subclasses.")
78
+
79
+ def _refine_sensitivity(self, iter_now: int, sensitivity: list[torch.Tensor]) -> list[torch.Tensor]:
80
+ """
81
+ Refine the sensitivity values based on the current iteration.
82
+
83
+ Parameters:
84
+ iter_now (int): The current iteration number.
85
+ sensitivity (list[torch.Tensor]): The sensitivity tensors to be refined.
86
+
87
+ Returns:
88
+ list[torch.Tensor]: The refined sensitivity tensors.
89
+ """
90
+ # This method can be overridden by subclasses if needed
91
+ return sensitivity
92
+
93
+ def closure(self, x: torch.Tensor, return_list: bool = False) -> torch.Tensor:
94
+ """
95
+ This method is called to update the design variables based on the optimization algorithm used.
96
+
97
+ Parameters:
98
+ x (torch.Tensor): The current design variables.
99
+ return_list (bool): If True, returns a list of objective function values; otherwise, returns their sum.
100
+
101
+ Returns:
102
+ torch.Tensor: The objective function value(s).
103
+ """
104
+ raise NotImplementedError("This method should be overridden by subclasses.")
105
+
106
+ def update_variables(self, dx: torch.Tensor) -> None:
107
+ """
108
+ Update the variables of the surfaces.
109
+ """
110
+ pass
@@ -0,0 +1,2 @@
1
+ from .update_geometry import UpdaterGeometries
2
+ from . import objectivefuncs
@@ -0,0 +1,5 @@
1
+ from .basefuncs import BaseConstraints, BaseObjective
2
+ from .shapederivative import ShapeDerivativeDisplacement
3
+ from .surfacefairness import Fairness
4
+ from . import boundarys
5
+ from .distancesurface import Distance
@@ -0,0 +1,117 @@
1
+ import torch
2
+
3
+
4
+ class BaseConstraints():
5
+ """
6
+ Base class for objective functions in MorphOpt.
7
+ """
8
+
9
+ def __init__(self):
10
+ """
11
+ Initialize the objective function.
12
+ """
13
+ self.scaler: list[torch.Tensor] = []
14
+ """
15
+ The scaler to process the objective function.
16
+ """
17
+
18
+
19
+ def initialize(self, r0: list[torch.Tensor], rdu0: list[torch.Tensor], rdu20: list[torch.Tensor], sensitivity: list[torch.Tensor], weights: list[torch.Tensor], *args, **kwargs):
20
+
21
+ self.scaler = []
22
+ for i in range(len(sensitivity)):
23
+ sen_now = sensitivity[i].reshape([-1, 3]).norm(dim=1)
24
+ if (sen_now != 0).any():
25
+ sen_now[sen_now==0] = sen_now[sen_now!=0].min()
26
+ else:
27
+ sen_now = torch.ones_like(sen_now) # Fallback if all zeros
28
+ self.scaler.append(sen_now * weights[i])
29
+
30
+ def __call__(self, r: list[torch.Tensor], rdu: list[torch.Tensor], rdu2: list[torch.Tensor], weights: list[torch.Tensor], *args, **kwargs) -> float:
31
+ """
32
+ Call the objective function.
33
+
34
+ Args:
35
+ weight (list[torch.Tensor]): The weights for each point.
36
+ r (list[torch.Tensor]): The point coordinates of the surfaces.
37
+ rdu (list[torch.Tensor]): The partial derivatives of the surfaces.
38
+ rdu2 (list[torch.Tensor]): The second partial derivatives of the surfaces.
39
+ *args: Positional arguments.
40
+ **kwargs: Keyword arguments.
41
+
42
+ Returns:
43
+ The value of the objective function.
44
+ """
45
+ raise NotImplementedError("Objective function not implemented.")
46
+
47
+ @staticmethod
48
+ def barrier_function(f: torch.Tensor, f_max: torch.Tensor|float, ratio: float, p: int):
49
+ """
50
+ Apply a barrier function to the objective function.
51
+
52
+ Args:
53
+ f (torch.Tensor): The objective function value.
54
+ f_max (float or torch.Tensor): The maximum value of the objective function.
55
+ ratio (float): The ratio for the barrier function.
56
+ p (float): The exponent for the barrier function.
57
+
58
+ Returns:
59
+ index (torch.Tensor): The indices of the elements that are greater than the barrier.
60
+ fnew (torch.Tensor): The new objective function value after applying the barrier function.
61
+
62
+ """
63
+ if type(f_max) != torch.Tensor:
64
+ f_max = torch.tensor(f_max).repeat(f.shape)
65
+ index = torch.where(f > f_max * ratio)[0]
66
+
67
+ if index.numel() > 0:
68
+ f = f[index]
69
+ f_max = f_max[index]
70
+ fnew = ((f - f_max * ratio) / (f_max - f_max * ratio))**(p)
71
+ else:
72
+ f = f[index]
73
+ fnew = torch.zeros_like(f)
74
+ return index, fnew
75
+
76
+
77
+ class BaseObjective():
78
+ """
79
+ Base class for objective functions in MorphOpt.
80
+ """
81
+
82
+ def __init__(self):
83
+ """
84
+ Initialize the objective function.
85
+ """
86
+ self.sensitivity: list[torch.Tensor] = []
87
+ """
88
+ Sensitivity of the shape derivative with respect to the contact forces.
89
+ """
90
+
91
+ def initialize(self, r0: list[torch.Tensor], rdu0: list[torch.Tensor], rdu20: list[torch.Tensor], weights: list[torch.Tensor], *args, **kwargs) -> None:
92
+ """
93
+ Initialize the objective function with the given parameters.
94
+
95
+ Args:
96
+ weights (list[torch.Tensor]): The weights for each point.
97
+ *args: Positional arguments.
98
+ **kwargs: Keyword arguments.
99
+ """
100
+ pass
101
+
102
+ def __call__(self, r: list[torch.Tensor], rdu: list[torch.Tensor], rdu2: list[torch.Tensor], *args, **kwargs) -> float:
103
+ """
104
+ Call the objective function.
105
+
106
+ Args:
107
+ weight (list[torch.Tensor]): The weights for each point.
108
+ r (list[torch.Tensor]): The point coordinates of the surfaces.
109
+ rdu (list[torch.Tensor]): The partial derivatives of the surfaces.
110
+ rdu2 (list[torch.Tensor]): The second partial derivatives of the surfaces.
111
+ *args: Positional arguments.
112
+ **kwargs: Keyword arguments.
113
+
114
+ Returns:
115
+ The value of the objective function.
116
+ """
117
+ raise NotImplementedError("Objective function not implemented.")
@@ -0,0 +1,91 @@
1
+
2
+ import torch
3
+ from .basefuncs import BaseConstraints
4
+
5
+
6
+ class MinRadius(BaseConstraints):
7
+ """
8
+ Cylinder boundary objective function for MorphOpt.
9
+ """
10
+
11
+ def __init__(self, radius: float) -> None:
12
+ """
13
+ Initialize the Cylinder objective function.
14
+ """
15
+ super().__init__()
16
+
17
+ self._radius = radius
18
+ """
19
+ The radius of the cylinder.
20
+ """
21
+
22
+
23
+ def __call__(self, r, rdu, rdu2, *args, **kwargs):
24
+
25
+ thre = 0.05
26
+ degree = 3
27
+
28
+ loss = torch.zeros(1, device=r[0].device, dtype=r[0].dtype)
29
+ for i in range(len(r)):
30
+
31
+ length_r = -(r[i][:, 0]**2 + r[i][:, 1]**2).sqrt() + self._radius
32
+
33
+ index_r, loss_r = self.barrier_function(length_r, thre, 0., degree)
34
+
35
+
36
+ if len(index_r) > 0:
37
+ loss = loss + (self.scaler[i][index_r] * loss_r).sum()
38
+
39
+ return loss
40
+
41
+
42
+ class Cylinder(BaseConstraints):
43
+ """
44
+ Cylinder boundary objective function for MorphOpt.
45
+ """
46
+
47
+ def __init__(self, radius: float, height: float, bottom: float) -> None:
48
+ """
49
+ Initialize the Cylinder objective function.
50
+ """
51
+ super().__init__()
52
+
53
+ self._radius = radius
54
+ """
55
+ The radius of the cylinder.
56
+ """
57
+
58
+ self._height = height + 1
59
+ """
60
+ The height of the cylinder.
61
+ """
62
+
63
+ self._bottom = bottom - 1
64
+ """
65
+ The bottom of the cylinder.
66
+ """
67
+
68
+ def __call__(self, r, rdu, rdu2, *args, **kwargs):
69
+
70
+ thre = 0.05
71
+ degree = 3
72
+
73
+ loss = torch.zeros(1, device=r[0].device, dtype=r[0].dtype)
74
+ for i in range(len(r)):
75
+
76
+ length_r = (r[i][:, 0]**2 + r[i][:, 1]**2).sqrt() - self._radius
77
+ length_bottom = -r[i][:, 2] + self._bottom
78
+ length_top = r[i][:, 2] - self._height
79
+
80
+ index_r, loss_r = self.barrier_function(length_r, thre, 0., degree)
81
+ index_bottom, loss_bottom = self.barrier_function(length_bottom, thre, 0., degree)
82
+ index_top, loss_top = self.barrier_function(length_top, thre, 0., degree)
83
+
84
+ if len(index_r) > 0:
85
+ loss = loss + (self.scaler[i][index_r] * loss_r).sum()
86
+ if len(index_bottom) > 0:
87
+ loss = loss + (self.scaler[i][index_bottom] * loss_bottom).sum()
88
+ if len(index_top) > 0:
89
+ loss = loss + (self.scaler[i][index_top] * loss_top).sum()
90
+
91
+ return loss
@@ -0,0 +1,123 @@
1
+
2
+ import torch
3
+ from .basefuncs import BaseConstraints
4
+ import logging
5
+
6
+ class Distance(BaseConstraints):
7
+ """
8
+ Distance objective function for MorphOpt.
9
+ """
10
+
11
+ def __init__(self, min_distance: list[list[float]]):
12
+ """
13
+ Initialize the Distance objective function with a name.
14
+ """
15
+ super().__init__()
16
+
17
+ self.neighbor_points: torch.Tensor
18
+ """
19
+ The neighbor points of the surfaces that need to be checked for distance.
20
+ """
21
+
22
+ self.points_surface_index: torch.Tensor
23
+ """
24
+ The index of the surfaces for each point.
25
+ """
26
+
27
+ self.neighbor_mindist: torch.Tensor
28
+ """
29
+ The allowable minimum distance between the points.
30
+ """
31
+
32
+ self.min_distance = torch.tensor(min_distance).to(torch.float32)
33
+ """
34
+ The minimum distance between the surfaces.
35
+ [i, j] the minimum distance between surface i and surface j.
36
+ """
37
+
38
+ self.distance_threshold = max(self.min_distance.flatten()).item() * 1.5
39
+ """
40
+ The distance threshold for the distance penalty.
41
+ """
42
+
43
+ def initialize(self, r0, rdu0, *args, **kwargs) -> None:
44
+ """
45
+ Initialize the Distance objective function with the surface points and their derivatives.
46
+ Parameters:
47
+ r0 (list[torch.Tensor]): List of surface points.
48
+ rdu0 (list[torch.Tensor]): List of surface point derivatives.
49
+ """
50
+
51
+ super().initialize(r0=r0, rdu0=rdu0, *args, **kwargs)
52
+
53
+ # prepare the normal vector for each surface
54
+ normal0 = []
55
+ for i in range(len(r0)):
56
+ normal_vector = torch.cross(rdu0[i][:, :, 1], rdu0[i][:, :, 0], dim=1)
57
+ normal_vector /= normal_vector.norm(dim=1, keepdim=True)
58
+ normal0.append(normal_vector.detach().clone())
59
+
60
+ num_points_total = []
61
+ index_Surf = []
62
+ for i in range(len(r0)):
63
+ index_Surf.append(torch.ones(r0[i].shape[0]) * i)
64
+ num_points_total += [r0[i].shape[0]]
65
+
66
+ normal0 = torch.cat(normal0, dim=0).type(torch.float32)
67
+
68
+ self.points_surface_index = torch.cat(index_Surf,
69
+ dim=0).type(torch.int64)
70
+
71
+
72
+ # get the neighbor points
73
+ import scipy.spatial
74
+ kdtree = scipy.spatial.KDTree(torch.cat(r0, dim=0).cpu().numpy())
75
+ self.neighbor_points = kdtree.query_pairs(r=self.distance_threshold, output_type='ndarray').T
76
+
77
+ self.neighbor_points = torch.from_numpy(self.neighbor_points).type(torch.int64).to(r0[0].device)
78
+
79
+ # determine the minimum distance between the neighbor points
80
+ self.neighbor_mindist = torch.zeros(self.neighbor_points.shape[1], dtype=torch.float32)
81
+ for i in range(self.min_distance.shape[0]):
82
+ for j in range(self.min_distance.shape[1]):
83
+ # Find indices where points are from surface i and j
84
+ index = torch.where(
85
+ (self.points_surface_index[self.neighbor_points[0]] == i) &
86
+ (self.points_surface_index[self.neighbor_points[1]] == j))[0]
87
+
88
+ if len(index) > 0:
89
+ if i == j: # Same surface case
90
+ # Use normal vector direction to determine distance for same surface
91
+ self.neighbor_mindist[index] = self.min_distance[i, j] * (-normal0[self.neighbor_points[0][index], :] *
92
+ normal0[self.neighbor_points[1][index], :]).sum(dim=1)
93
+ else:
94
+ # Different surfaces
95
+ self.neighbor_mindist[index] = self.min_distance[i, j]
96
+
97
+ index_pos = self.neighbor_mindist > 0
98
+ self.neighbor_points = self.neighbor_points[:, index_pos]
99
+ self.neighbor_mindist = self.neighbor_mindist[index_pos]
100
+
101
+ logging.debug(f"Distance Objective Function: {self.neighbor_points.shape[1]} point pairs within threshold {self.distance_threshold}")
102
+
103
+ def __call__(self, r, *args, **kwargs):
104
+
105
+ R = torch.cat(r, dim=0).type(torch.float32)
106
+ thre = 0.02
107
+ degree = 5
108
+ loss_distance = torch.tensor(0.0, dtype=torch.float32)
109
+ weight_flatten = torch.cat(self.scaler, dim=0).type(torch.float32)
110
+ if torch.numel(self.neighbor_points) != 0:
111
+
112
+ deltaR = R[self.neighbor_points[0], :] - R[self.neighbor_points[1], :]
113
+ distance = torch.sqrt(((deltaR)**2).sum(dim=1))
114
+
115
+ dist = self.neighbor_mindist - distance + thre
116
+
117
+ indexl, l = self.barrier_function(dist, thre, 0, degree)
118
+
119
+ if len(indexl) > 0:
120
+ loss_distance += (l * weight_flatten[self.neighbor_points[0, indexl]] *
121
+ weight_flatten[self.neighbor_points[1, indexl]]).sum()
122
+
123
+ return loss_distance
@@ -0,0 +1,179 @@
1
+
2
+
3
+ from scipy import interpolate
4
+ import torch
5
+ from .basefuncs import BaseConstraints
6
+ import morphopt
7
+
8
+ class ShapeDerivativeDisplacement(BaseConstraints):
9
+ """
10
+ Shape derivative for contact forces.
11
+ """
12
+
13
+ def __init__(self):
14
+
15
+ super().__init__()
16
+
17
+ self._r0: list[torch.Tensor] = []
18
+ """
19
+ Initial reference configuration.
20
+ """
21
+
22
+ def initialize(self, r0: list[torch.Tensor], *args, **kwargs):
23
+
24
+ objfun = morphopt.controller.objfun
25
+ fe = objfun.fe
26
+
27
+ self._r0 = [r0[i].detach().clone() for i in range(len(r0))]
28
+
29
+ ins = fe.assembly.get_instance('final_model')
30
+ part = fe.assembly.get_part('final_model')
31
+ grad_pos = torch.zeros_like(ins.nodes)
32
+
33
+ def closure_work(nodes_diff: torch.Tensor):
34
+ nodes0 = part.nodes
35
+ part.nodes = nodes_diff
36
+ fe.initialize()
37
+
38
+ # compute the sensitivity of the displacement
39
+ work = objfun.get_objective().to(part.nodes.device)
40
+ for i in range(len(objfun.U)):
41
+ GC0 = objfun.U[i].to(part.nodes.device)
42
+ fe.assembly.GC = GC0
43
+ fe.assembly.RGC = fe.assembly._GC2RGC(GC0)
44
+ morphopt.controller.params.feamodel.process_fea(fe=fe, step_index=i)
45
+
46
+ R = fe.assembly.assemble_Stiffness_Matrix(GC=GC0)[0]
47
+ ADJu = objfun.ADJu[i].to(part.nodes.device)
48
+ work = work + (R*ADJu).sum()
49
+ part.nodes = nodes0
50
+ fe.initialize()
51
+ return work
52
+ grad_pos += torch.autograd.functional.jacobian(closure_work, part.nodes.detach().clone())
53
+
54
+ i=0
55
+
56
+ interpolate_points = self._get_interpolate_points()
57
+
58
+ self.sensitivity = self._sensitivity_interpolation(Ldot=grad_pos, points_request=part.nodes, interpolated_points=interpolate_points)
59
+
60
+
61
+ def show_sensitivity(self, ind: int) -> None:
62
+ """
63
+ Show the shape sensitivity
64
+ """
65
+
66
+ import pyvista as pv
67
+ import numpy as np
68
+
69
+ plotter = pv.Plotter(window_size=(1000, 1000))
70
+ plotter.set_background('white')
71
+
72
+ r = self._r0[ind].detach().cpu().numpy()
73
+ n = self.sensitivity[ind].detach().cpu().numpy()
74
+
75
+ # r and n are [N, 3]
76
+ points = r
77
+ vectors = n
78
+
79
+ # Debug: print statistics
80
+ print(f"Points shape: {points.shape}")
81
+ print(f"Vectors shape: {vectors.shape}")
82
+ print(f"Vector magnitude range: [{np.linalg.norm(vectors, axis=1).min():.6e}, {np.linalg.norm(vectors, axis=1).max():.6e}]")
83
+
84
+ # Filter out zero vectors
85
+ vector_norms = np.linalg.norm(vectors, axis=1)
86
+ valid_mask = vector_norms > 1e-10
87
+
88
+ if valid_mask.sum() == 0:
89
+ print("Warning: All sensitivity vectors are zero!")
90
+ return
91
+
92
+ points_filtered = points[valid_mask]
93
+ vectors_filtered = vectors[valid_mask]
94
+
95
+ # Auto-scale magnitude based on model size
96
+ model_size = np.max(points_filtered.max(axis=0) - points_filtered.min(axis=0))
97
+ vector_mag = np.linalg.norm(vectors_filtered, axis=1).mean()
98
+ mag_scale = model_size / vector_mag * 0.01 # Scale to 1% of model size
99
+
100
+ print(f"Using {valid_mask.sum()} non-zero vectors out of {len(valid_mask)}")
101
+ print(f"Auto-scaled magnitude: {mag_scale:.6e}")
102
+
103
+ # Add surface mesh for context
104
+ plotter.add_points(points_filtered, color='blue', point_size=5, render_points_as_spheres=True)
105
+
106
+ # Add arrows with auto-scaled magnitude
107
+ plotter.add_arrows(points_filtered, vectors_filtered, mag=mag_scale, color='red')
108
+
109
+ plotter.show_axes()
110
+ plotter.show()
111
+
112
+ def __call__(self, r: list[torch.Tensor], *args, **kwargs):
113
+ loss_objective = 0.0
114
+
115
+ for i in range(len(self.sensitivity)):
116
+ # r[i], self._r0[i], self.sensitivity[i] are all [p, 3]
117
+ # Compute inner product: sum over all points and dimensions
118
+ loss_objective += ((r[i] - self._r0[i]) * self.sensitivity[i]).sum()
119
+
120
+ return loss_objective
121
+
122
+ def _get_interpolate_points(self, *args, **kwargs):
123
+ """
124
+ Get the interpolated points for the design variables.
125
+
126
+ Parameters:
127
+ *args: Additional arguments.
128
+ **kwargs: Additional keyword arguments.
129
+
130
+ Returns:
131
+ list[torch.Tensor]: The interpolated points for the design variables.
132
+ """
133
+ surfaces = morphopt.controller.params.geometry
134
+ r0 = surfaces.get_geometry_values()[0]
135
+ interpolated_points = []
136
+ for sf in range(surfaces.num_surface):
137
+ interpolated_points.append(r0[sf].cpu().numpy())
138
+ return interpolated_points
139
+
140
+ def _sensitivity_interpolation(
141
+ self, Ldot: torch.Tensor, points_request: torch.Tensor,
142
+ interpolated_points: list[torch.Tensor]) -> list[torch.Tensor]:
143
+ """
144
+ Interpolate the sensitivity into the structural grids.
145
+
146
+ Parameters:
147
+ Ldot (torch.Tensor): The sensitivity values for elements gaussian points.
148
+ [num_points, 3]
149
+ points_request (torch.Tensor): The coordinates of the elements.
150
+ [num_points, 3]
151
+ interpolated_points (list[torch.Tensor]): The coordinates of the interpolation points.
152
+ [num_surface, 3]
153
+
154
+ Returns:
155
+ list[torch.Tensor]: The interpolated sensitivity values for the structural grids.
156
+ [num_surface, sensitivity]
157
+ """
158
+
159
+ # interpolate the sensitivity into the structral grids
160
+ output_senNodes = []
161
+
162
+ for surf_index in range(len(interpolated_points)):
163
+
164
+ output_senNodes.append(torch.zeros(interpolated_points[surf_index].shape))
165
+
166
+ for i in range(3):
167
+ Part_B = interpolate.griddata(
168
+ points_request.reshape([-1, 3]).detach().cpu().numpy(),
169
+ Ldot[:, i].flatten().detach().cpu().numpy(),
170
+ (interpolated_points[surf_index][:, 0],
171
+ interpolated_points[surf_index][:, 1],
172
+ interpolated_points[surf_index][:, 2]),
173
+ method='nearest',
174
+ fill_value=0,
175
+ rescale=True)
176
+
177
+ output_senNodes[-1][:, i] = torch.tensor((Part_B).tolist())
178
+
179
+ return output_senNodes
@@ -0,0 +1,40 @@
1
+ import torch
2
+ from .basefuncs import BaseConstraints
3
+ from ....modelparams import GeometryParams
4
+ from . import ShapeDerivativeDisplacement
5
+ class Fairness(BaseConstraints):
6
+ """
7
+ Fairness objective function for MorphOpt.
8
+ """
9
+
10
+ def __init__(self, surfaces: GeometryParams, sensitivity: ShapeDerivativeDisplacement):
11
+ """
12
+ Initialize the fairness objective function with a name.
13
+ """
14
+ super().__init__()
15
+ self.surfaces = surfaces
16
+ """
17
+ The surfaces object that contains the design variables.
18
+ """
19
+
20
+ self.sensitivity = sensitivity
21
+ """the sensitivity to anchor the scaler"""
22
+
23
+ def __call__(self, r: list[torch.Tensor], rdu: list[torch.Tensor], rdu2: list[torch.Tensor], *args, **kwargs) -> float:
24
+ """
25
+ Call the fairness objective function.
26
+
27
+ Args:
28
+ r (list[torch.Tensor]): The point coordinates of the surfaces.
29
+ rdu (list[torch.Tensor]): The partial derivatives of the surfaces.
30
+ rdu2 (list[torch.Tensor]): The second partial derivatives of the surfaces.
31
+ *args: Positional arguments.
32
+ **kwargs: Keyword arguments.
33
+
34
+ Returns:
35
+ The value of the fairness objective function.
36
+ """
37
+
38
+
39
+ # Implement the fairness objective function here
40
+ return sum(self.surfaces.get_penalty_fairness(self.scaler, r, rdu, rdu2))