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,440 @@
1
+ import csv
2
+ import numpy as np
3
+ import torch
4
+ from .baseobject import BaseObject
5
+
6
+ class History(BaseObject):
7
+ """
8
+ A class to record the information of the optimization process.
9
+ """
10
+
11
+ def __init__(self):
12
+ self.data: dict[str, np.ndarray] = {}
13
+ self.iteration: int = 0
14
+
15
+ def initialize(self) -> None:
16
+ self.iteration = 0
17
+ self.data = {}
18
+
19
+ def append(self, name: str, value: any) -> None:
20
+ """
21
+ Append a value to the history data.
22
+ """
23
+ if value is None:
24
+ return
25
+
26
+ # Helper to safely convert torch tensors to numpy
27
+ def to_numpy(val):
28
+ if isinstance(val, torch.Tensor):
29
+ return val.detach().cpu().numpy()
30
+ return val
31
+
32
+ if isinstance(value, list):
33
+ value = [to_numpy(v) for v in value]
34
+ else:
35
+ value = to_numpy(value)
36
+
37
+ new_val = np.array(value)
38
+ if name not in self.data:
39
+ if new_val.ndim == 0:
40
+ # Scalar -> 1D array
41
+ self.data[name] = new_val.reshape(1)
42
+ else:
43
+ # Array -> (1, ...) array
44
+ self.data[name] = new_val.reshape(1, *new_val.shape)
45
+ else:
46
+ # stack on the first axis (0)
47
+ target_shape = (1, *new_val.shape)
48
+ self.data[name] = np.concatenate((self.data[name], new_val.reshape(target_shape)), axis=0)
49
+
50
+ if name == 'objective':
51
+ self.iteration = len(self.data[name])
52
+
53
+ @property
54
+ def history_objective(self):
55
+ return self.data.get('objective', np.array([]))
56
+
57
+ @history_objective.setter
58
+ def history_objective(self, val):
59
+ self.data['objective'] = np.array(val)
60
+ self.iteration = len(val)
61
+
62
+ @property
63
+ def history_time(self):
64
+ return self.data.get('time', np.array([]))
65
+
66
+ @history_time.setter
67
+ def history_time(self, val):
68
+ self.data['time'] = np.array(val)
69
+
70
+ @property
71
+ def history_num_elements(self):
72
+ return self.data.get('num_elements', np.array([]))
73
+
74
+ @history_num_elements.setter
75
+ def history_num_elements(self, val):
76
+ self.data['num_elements'] = np.array(val)
77
+
78
+ @property
79
+ def history_num_nodes(self):
80
+ return self.data.get('num_nodes', np.array([]))
81
+
82
+ @history_num_nodes.setter
83
+ def history_num_nodes(self, val):
84
+ self.data['num_nodes'] = np.array(val)
85
+
86
+ @property
87
+ def history_deformation(self):
88
+ return self.data.get('deformation', np.array([]))
89
+
90
+ @history_deformation.setter
91
+ def history_deformation(self, val):
92
+ self.data['deformation'] = np.array(val)
93
+
94
+ @property
95
+ def history_metrics(self):
96
+ return self.data.get('metrics', np.array([]))
97
+
98
+ @history_metrics.setter
99
+ def history_metrics(self, val):
100
+ self.data['metrics'] = np.array(val)
101
+
102
+ def save(self, foldpath: str, *args, **kwargs) -> None:
103
+ """
104
+ Save the history to a CSV file.
105
+ CSV format:
106
+ - Row 1: Column headers (iteration, objective, T0, T1, ..., U0-0, U0-1, ..., UdF0-0-0, UdF0-0-1, ...)
107
+ - Row 2+: Data records
108
+ """
109
+ filepath = foldpath + '/history_record.csv'
110
+
111
+ # Determine keys and order
112
+ # Fixed order for known keys, then others
113
+ standard_keys = ['objective', 'metrics', 'time', 'num_elements', 'num_nodes', 'deformation']
114
+ other_keys = [k for k in self.data.keys() if k not in standard_keys]
115
+ all_keys = standard_keys + sorted(other_keys)
116
+
117
+ # Filter keys that actually exist
118
+ target_keys = [k for k in all_keys if k in self.data and len(self.data[k]) > 0]
119
+
120
+ # Prepare headers
121
+ headers = ['iteration']
122
+ key_dims = {} # Store dimensions for each key to handle flattening
123
+
124
+ for k in target_keys:
125
+ arr = self.data[k]
126
+ # check shape of the content (excluding iteration dim)
127
+ item_shape = arr.shape[1:]
128
+
129
+ if len(item_shape) == 0:
130
+ # Scalar per iteration
131
+ headers.append(k)
132
+ key_dims[k] = 0
133
+ elif len(item_shape) == 1:
134
+ # 1D array per iteration
135
+ for i in range(item_shape[0]):
136
+ if k == 'time':
137
+ headers.append(f'T{i}')
138
+ else:
139
+ headers.append(f'{k}_{i}')
140
+ key_dims[k] = 1
141
+ else:
142
+ # Multi-dim array per iteration - flatten
143
+ flat_size = np.prod(item_shape)
144
+ if k == 'deformation':
145
+ if len(item_shape) == 2:
146
+ for r in range(item_shape[0]):
147
+ for c in range(item_shape[1]):
148
+ headers.append(f'U{r}-{c}')
149
+ else:
150
+ for i in range(flat_size):
151
+ headers.append(f'{k}_{i}')
152
+ else:
153
+ for i in range(flat_size):
154
+ headers.append(f'{k}_{i}')
155
+ key_dims[k] = item_shape
156
+
157
+ with open(filepath, 'w', newline='', encoding='utf-8') as csvfile:
158
+ writer = csv.writer(csvfile)
159
+ writer.writerow(headers)
160
+
161
+ # Write rows
162
+ max_len = 0
163
+ for k in target_keys:
164
+ max_len = max(max_len, len(self.data[k]))
165
+
166
+ for i in range(max_len):
167
+ row = [i+1]
168
+ for k in target_keys:
169
+ arr = self.data[k]
170
+ if i < len(arr):
171
+ val = arr[i]
172
+ if key_dims[k] == 0:
173
+ row.append(val)
174
+ else:
175
+ # Flatten
176
+ row.extend(val.flatten().tolist())
177
+ else:
178
+ # Pad with empty
179
+ shape = key_dims[k]
180
+ if shape == 0:
181
+ size = 1
182
+ elif isinstance(shape, int):
183
+ size = shape
184
+ else:
185
+ size = np.prod(shape)
186
+ row.extend([''] * size)
187
+
188
+ # Formatting
189
+ formatted_row = []
190
+ for idx, val in enumerate(row):
191
+ if idx == 0:
192
+ formatted_row.append(val)
193
+ elif isinstance(val, (float, np.floating)):
194
+ formatted_row.append(f"{val:.4e}")
195
+ else:
196
+ formatted_row.append(val)
197
+ writer.writerow(formatted_row)
198
+
199
+ def load(self, foldpath: str, iteration: int = None) -> None:
200
+ """
201
+ Load the history from a CSV file.
202
+ """
203
+ filepath = foldpath + '/history_record.csv'
204
+ self.initialize()
205
+
206
+ try:
207
+ with open(filepath, 'r', encoding='utf-8') as csvfile:
208
+ reader = csv.reader(csvfile)
209
+ try:
210
+ headers = next(reader)
211
+ except StopIteration:
212
+ return
213
+
214
+ col_map = []
215
+
216
+ for h in headers:
217
+ if h == 'iteration':
218
+ col_map.append(None)
219
+ elif h == 'objective':
220
+ col_map.append(('objective', None))
221
+ elif h == 'num_elements':
222
+ col_map.append(('num_elements', None))
223
+ elif h == 'num_nodes':
224
+ col_map.append(('num_nodes', None))
225
+ elif h.startswith('T') and h[1:].isdigit():
226
+ col_map.append(('time', int(h[1:])))
227
+ elif h.startswith('U') and '-' in h:
228
+ col_map.append(('deformation', h))
229
+ else:
230
+ if '_' in h:
231
+ parts = h.rsplit('_', 1)
232
+ if parts[1].isdigit():
233
+ col_map.append((parts[0], int(parts[1])))
234
+ else:
235
+ col_map.append((h, None))
236
+ else:
237
+ col_map.append((h, None))
238
+
239
+ rows = list(reader)
240
+ except FileNotFoundError:
241
+ return
242
+
243
+ if not rows:
244
+ return
245
+
246
+ # Temporary lists
247
+ data_lists = {
248
+ 'objective': [],
249
+ 'time': [],
250
+ 'num_elements': [],
251
+ 'num_nodes': [],
252
+ 'deformation': [],
253
+ 'metrics': []
254
+ }
255
+
256
+ # Helper to classify scalars vs arrays
257
+ # Fixed classifications
258
+ scalar_keys = {'objective', 'num_elements', 'num_nodes'}
259
+
260
+ # Analyze col_map to classify other keys
261
+ # Collect subs for each key to determine if it's scalar or array
262
+ key_subs = {}
263
+ for item in col_map:
264
+ if item is None: continue
265
+ k, sub = item
266
+ if k not in key_subs: key_subs[k] = set()
267
+ key_subs[k].add(sub)
268
+
269
+ # --- Correction Logic Start ---
270
+ # If a key seems to be an array (has subs), but does not have index 0,
271
+ # it is likely a scalar variable that happens to end with "_N" (e.g. "param_1").
272
+ keys_to_revert = set()
273
+ for k, subs in key_subs.items():
274
+ if k in ['time', 'deformation']: continue # special arrays
275
+ if k in scalar_keys: continue
276
+
277
+ # If it contains None, it's already treated as scalar/mixed.
278
+ if None in subs: continue
279
+
280
+ # Check if 0 is present in the subs
281
+ # Note: subs contains integers for generic arrays
282
+ if 0 not in subs:
283
+ keys_to_revert.add(k)
284
+
285
+ if keys_to_revert:
286
+ new_col_map = []
287
+ for i, item in enumerate(col_map):
288
+ if item is None:
289
+ new_col_map.append(None)
290
+ else:
291
+ k, sub = item
292
+ if k in keys_to_revert:
293
+ # Revert to using the original header as the key
294
+ full_header_name = headers[i]
295
+ new_col_map.append((full_header_name, None))
296
+ else:
297
+ new_col_map.append(item)
298
+ col_map = new_col_map
299
+
300
+ # Re-generate key_subs after correction
301
+ key_subs = {}
302
+ for item in col_map:
303
+ if item is None: continue
304
+ k, sub = item
305
+ if k not in key_subs: key_subs[k] = set()
306
+ key_subs[k].add(sub)
307
+ # --- Correction Logic End ---
308
+
309
+ for k in key_subs:
310
+ if k not in data_lists:
311
+ data_lists[k] = []
312
+
313
+ # Determine which are scalars (all subs are None)
314
+ # Exception: deformation is always array (sub like U0-0)
315
+ # Exception: time is always array (sub 0, 1...)
316
+
317
+ for k, subs in key_subs.items():
318
+ if k in scalar_keys: continue
319
+ if k == 'deformation' or k == 'time': continue
320
+
321
+ if len(subs) == 1 and list(subs)[0] is None:
322
+ scalar_keys.add(k)
323
+
324
+
325
+ # Determine shape for deformation
326
+ def_indices = [h for h in headers if h.startswith('U') and '-' in h]
327
+ max_r, max_c = 0, 0
328
+ if def_indices:
329
+ for h in def_indices:
330
+ parts = h.replace('U','').split('-')
331
+ if len(parts) == 2:
332
+ try:
333
+ r, c = int(parts[0]), int(parts[1])
334
+ max_r = max(max_r, r)
335
+ max_c = max(max_c, c)
336
+ except: pass
337
+ def_shape = (max_r+1, max_c+1)
338
+ else:
339
+ def_shape = None
340
+
341
+ # Determine shape for generic arrays (to handle missing rows)
342
+ array_widths = {}
343
+ for k, subs in key_subs.items():
344
+ if k in scalar_keys or k == 'deformation': continue
345
+ max_idx = -1
346
+ for s in subs:
347
+ try:
348
+ idx = int(s)
349
+ max_idx = max(max_idx, idx)
350
+ except (ValueError, TypeError):
351
+ pass
352
+ if max_idx >= 0:
353
+ array_widths[k] = max_idx + 1
354
+ else:
355
+ array_widths[k] = 0
356
+
357
+ for row in rows:
358
+ if not row: continue
359
+
360
+ # Init row data container
361
+ row_data = {}
362
+ for k in data_lists:
363
+ if k in scalar_keys:
364
+ row_data[k] = None
365
+ else:
366
+ row_data[k] = {}
367
+
368
+ for i, val_str in enumerate(row):
369
+ if i >= len(col_map) or col_map[i] is None: continue
370
+ if val_str == '': continue
371
+
372
+ try: val = float(val_str)
373
+ except: val = val_str # Fallback strings
374
+
375
+ key, sub = col_map[i]
376
+
377
+ if key in scalar_keys:
378
+ if key in ['num_elements', 'num_nodes']:
379
+ try: row_data[key] = int(val)
380
+ except: row_data[key] = val
381
+ else:
382
+ row_data[key] = val
383
+ else:
384
+ # Array type
385
+ if key == 'deformation':
386
+ parts = sub.replace('U','').split('-')
387
+ r, c = int(parts[0]), int(parts[1])
388
+ row_data[key][(r,c)] = val
389
+ else:
390
+ idx = sub if sub is not None else 0
391
+ row_data[key][idx] = val
392
+
393
+ # Convert row_data to list elements
394
+ for k, v in row_data.items():
395
+ if k in scalar_keys:
396
+ if v is not None:
397
+ data_lists[k].append(v)
398
+ else:
399
+ # Append NaN for missing scalar to maintain alignment
400
+ data_lists[k].append(np.nan)
401
+
402
+ elif k == 'deformation':
403
+ if def_shape and v:
404
+ d_mat = np.zeros(def_shape)
405
+ for (r,c), val in v.items():
406
+ d_mat[r, c] = val
407
+ data_lists[k].append(d_mat)
408
+ elif def_shape:
409
+ # Append matrix of NaNs
410
+ data_lists[k].append(np.full(def_shape, np.nan))
411
+ elif v:
412
+ pass
413
+ else:
414
+ # Generic Array
415
+ if v:
416
+ max_idx = max(v.keys()) if all(isinstance(x, int) for x in v.keys()) else len(v)-1
417
+ vec = [v.get(x, 0.0) for x in range(max_idx+1)]
418
+ data_lists[k].append(vec)
419
+ elif k in array_widths and array_widths[k] > 0:
420
+ # Append list of NaNs
421
+ data_lists[k].append([np.nan] * array_widths[k])
422
+ else:
423
+ # If unknown width or not in headers, append empty?
424
+ # Or better, if it's a default key like 'time' but not in CSV,
425
+ # we can append empty list (which leads to 0-width array).
426
+ data_lists[k].append([])
427
+
428
+ # Convert to arrays
429
+
430
+ for k, v in data_lists.items():
431
+ if v:
432
+ self.data[k] = np.array(v)
433
+
434
+ # Iteration slice
435
+ if iteration is not None:
436
+ for k in self.data:
437
+ self.data[k] = self.data[k][:iteration]
438
+
439
+ if 'objective' in self.data:
440
+ self.iteration = len(self.data['objective'])
@@ -0,0 +1,4 @@
1
+ from .geometry.geometryparams import GeometryParams
2
+ from .feamodel.feaparams import FEAParams
3
+ from .materials.materialparams import Materials
4
+ from .params import Params
@@ -0,0 +1,97 @@
1
+
2
+
3
+ import torch
4
+ from ..baseobject import BaseObject
5
+
6
+ class BaseParams(BaseObject):
7
+ """
8
+ Base class for all parameter classes.
9
+ """
10
+ def __init__(self, **kwargs):
11
+ """
12
+ Initialize the parameters with the given keyword arguments.
13
+ """
14
+ self.__dict__.update(kwargs)
15
+
16
+ def __repr__(self):
17
+ """
18
+ Return a string representation of the parameters.
19
+ """
20
+ return f"{self.__class__.__name__}({self.__dict__})"
21
+
22
+ def __str__(self):
23
+ """
24
+ Return a string representation of the parameters.
25
+ """
26
+ return self.__repr__()
27
+
28
+ def reinitialize(self, iteration: int, *args, **kwargs) -> None:
29
+ """
30
+ reInitialize the parameters.
31
+
32
+ This method should be implemented in subclasses to initialize specific parameters.
33
+ """
34
+ pass
35
+
36
+ def initialize(self, *args, **kwargs) -> None:
37
+ """
38
+ Initialize the parameters.
39
+
40
+ This method should be implemented in subclasses to initialize specific parameters.
41
+ """
42
+ pass
43
+
44
+ def get_variables(self) -> torch.Tensor:
45
+ """
46
+ Get the zeros of the parameters.
47
+
48
+ Returns:
49
+ torch.Tensor: The variables of the parameters.
50
+ """
51
+ raise NotImplementedError("This method should be implemented in subclasses.")
52
+
53
+ def update_variables(self, x_change: torch.Tensor) -> None:
54
+ """
55
+ Update the variables of the class.
56
+
57
+ Args:
58
+ x_change (torch.Tensor): The change in variables.
59
+ """
60
+ raise NotImplementedError("This method should be implemented in subclasses.")
61
+
62
+ def set_parameters(self, xlist: list[torch.Tensor]) -> None:
63
+ """
64
+ Set the parameters of the class.
65
+ the new parameters are expected to be in a flattened format with clone and detach.
66
+
67
+ Args:
68
+ xlist (list[torch.Tensor]): The new parameters for the class.
69
+ """
70
+ raise NotImplementedError("This method should be implemented in subclasses.")
71
+
72
+ def get_parameters(self) -> list[torch.Tensor]:
73
+ """
74
+ Get the parameters of the class.
75
+ the parameters are expected to be in a flattened format with clone and detach.
76
+
77
+ Returns:
78
+ list[torch.Tensor]: The parameters of the class.
79
+ """
80
+ raise NotImplementedError("This method should be implemented in subclasses.")
81
+
82
+ def plot(self) -> None:
83
+ """
84
+ Plot the parameters.
85
+
86
+ This method should be implemented in subclasses to plot specific parameters.
87
+ """
88
+ raise NotImplementedError("This method should be implemented in subclasses.")
89
+
90
+ def _export_data(self, foldpath: str):
91
+ """
92
+ Export the data of parameters to file(s).
93
+
94
+ Args:
95
+ foldpath (str): The path to export the data.
96
+ """
97
+ pass
File without changes
@@ -0,0 +1,10 @@
1
+ from .basefeainterface import BaseFEAInterface
2
+ from .pressureinterface import PressureInterface
3
+ from .contactinterface import ContactInterface, ContactSelfInterface
4
+ from .pointinterface import ConcentratedForceInterface, ConcentratedMomentInterface
5
+ from .springinterface import SpringToGroundInterface, SpringBetweenRPsInterface
6
+ from .coupleinterface import CoupleInterface
7
+ from .boundaryconditioninterface import BoundaryConditionInterface, BoundaryConditionRPInterface
8
+ from .referencepointinterface import ReferencePointInterface
9
+
10
+ from .bodyforceinterface import BodyforceInterface
@@ -0,0 +1,50 @@
1
+
2
+
3
+ import numpy as np
4
+ import torch
5
+ from torchfea import FEAController
6
+
7
+ class BaseFEAInterface:
8
+ """
9
+ Base class for fea interfaces.
10
+ This class is not meant to be instantiated directly.
11
+ It provides a common interface for all fea interfaces.
12
+
13
+ all data cannot be cuda tensors
14
+ """
15
+
16
+ def __init__(self):
17
+ """
18
+ Initialize the base interface and optional parameters.
19
+ """
20
+
21
+ self._values: list[float] = np.zeros(self.num_values).tolist()
22
+ """List of fea parameter values."""
23
+
24
+ self._name: str = ""
25
+ """Name of the interface."""
26
+
27
+ @property
28
+ def num_values(self) -> int:
29
+ """
30
+ Get the number of fea variables.
31
+
32
+ Returns:
33
+ int: The number of fea variables.
34
+ """
35
+ raise NotImplementedError("This method should be implemented in subclasses.")
36
+
37
+ def modify_fea(self, fe: FEAController, name: str) -> None:
38
+ """
39
+ create the object in FEA assembly
40
+
41
+ Args:
42
+ fe (FEAController): The FEA controller instance.
43
+ """
44
+ raise NotImplementedError("This method should be implemented in subclasses.")
45
+
46
+ def apply_fea_value(self, fe: FEAController, name: str) -> None:
47
+ """
48
+ Apply the values to the FEA object.
49
+ """
50
+ pass
@@ -0,0 +1,71 @@
1
+ from torchfea import FEAController
2
+ import torch
3
+ from .basefeainterface import BaseFEAInterface
4
+
5
+ from torchfea.model.loads.body_force import BodyForce
6
+
7
+
8
+ class BodyforceInterface(BaseFEAInterface):
9
+ """
10
+ Body force (Gravity) load interface.
11
+
12
+ Values (list[float], length=3):
13
+ - [0] fx
14
+ - [1] fy
15
+ - [2] fz
16
+ """
17
+
18
+ def __init__(self, element_name: str, instance_name: str = 'final_model'):
19
+ """
20
+ Initialize the BodyforceInterface class.
21
+ """
22
+ super().__init__()
23
+ self.instance_name = instance_name
24
+ self.element_name = element_name
25
+
26
+ @property
27
+ def force_density(self) -> list[float]:
28
+ """
29
+ Get the force density values.
30
+
31
+ Returns:
32
+ list[float]: The force density values [fx, fy, fz].
33
+ """
34
+ return self._values
35
+
36
+ @force_density.setter
37
+ def force_density(self, value: list[float]) -> None:
38
+ """
39
+ Set the force density values.
40
+
41
+ Args:
42
+ value (list[float]): The new force density values [fx, fy, fz].
43
+ """
44
+ if len(value) != 3:
45
+ raise ValueError("Force density must be a list of 3 floats.")
46
+ self._values = [float(v) for v in value]
47
+
48
+ @property
49
+ def num_values(self) -> int:
50
+ """
51
+ Get the number of variables.
52
+
53
+ Returns:
54
+ int: The number of variables (3 for fx, fy, fz).
55
+ """
56
+ return 3
57
+
58
+ def modify_fea(self, fe: FEAController, name: str) -> None:
59
+ body_force = BodyForce(instance_name=self.instance_name, element_name=self.element_name, force_density=self.force_density)
60
+ fe.assembly.add_load(body_force, name)
61
+
62
+ def apply_fea_value(self, fe: FEAController, name: str) -> None:
63
+ body_force: BodyForce = fe.assembly.get_load(name)
64
+ device = body_force.force_density.device
65
+ body_force.force_density = torch.tensor(self.force_density, dtype=torch.float64, device=device)
66
+
67
+ # Update cached values if initialized
68
+ if hasattr(body_force, '_element'):
69
+ body_force._pdU_values = torch.einsum('i, ge, gea->eai', body_force.force_density, body_force._element.gaussian_weight, body_force._element.shape_function_d0_gaussian).flatten()
70
+
71
+