v0.16.0
Loading...
Searching...
No Matches
ObjectiveFunctionData.cpp
Go to the documentation of this file.
1/**
2 * @file ObjectiveFunctionData.cpp
3 * @brief Implementation of Python-integrated objective function interface
4 * @details Implements Python-C++ bridge for objective function evaluation
5 * using Boost.Python and NumPy for topology optimization
6 */
7
8#ifdef ENABLE_PYTHON_BINDING
9#include <boost/python.hpp>
10#include <boost/python/def.hpp>
11#include <boost/python/numpy.hpp>
12namespace bp = boost::python;
13namespace np = boost::python::numpy;
14#endif
15
16#include <MoFEM.hpp>
17using namespace MoFEM;
19
21
22#ifdef ENABLE_PYTHON_BINDING
23
24constexpr int SPACE_DIM =
25 EXECUTABLE_DIMENSION; ///< Space dimension of problem (2D or 3D), set at compile time
26
27/**
28 * @brief Implementation of ObjectiveFunctionData interface using Python integration
29 *
30 * This class provides a concrete implementation of the ObjectiveFunctionData interface
31 * that bridges MoFEM C++ data structures with Python-defined objective functions.
32 * It enables flexible definition of optimization objectives through Python scripting
33 * while maintaining high-performance computation in the C++ finite element framework.
34 *
35 * Key features:
36 * - Python function evaluation with automatic data conversion
37 * - Support for objective function and gradient computations
38 * - Topology optimization mode definition through Python
39 * - Efficient NumPy array interfacing for large data sets
40 * - Automatic memory management between C++ and Python
41 *
42 * The class handles:
43 * 1. Loading and executing Python objective function scripts
44 * 2. Converting MoFEM data structures to NumPy arrays
45 * 3. Calling Python functions for objective evaluation
46 * 4. Converting Python results back to MoFEM format
47 * 5. Managing Python interpreter state and namespace
48 *
49 * Example Python interface functions that must be defined:
50 * - objectiveInteriorFunction(coords, u, stress, strain) -> objective_value
51 * - objectiveInteriorGradientStress(coords, u, stress, strain) -> gradient_wrt_stress
52 * - numberOfModes(block_id) -> number_of_topology_modes
53 * - blockModes(block_id, coords, centroid, bbox) -> mode_vectors
54 */
55struct ObjectiveFunctionDataImpl : public ObjectiveFunctionData {
56 ObjectiveFunctionDataImpl() = default;
57 virtual ~ObjectiveFunctionDataImpl() = default;
58
59 /// Initialize Python interpreter and load objective function script
60 MoFEMErrorCode initPython(const std::string py_file);
61
62 /**
63 * @brief Evaluate objective function at current state
64 *
65 * Calls Python-defined objective function with current displacement,
66 * stress, and strain fields. Used during optimization to compute
67 * the objective value that drives the optimization process.
68 *
69 * @param coords Gauss point coordinates
70 * @param u_ptr Displacement field values
71 * @param stress_ptr Stress tensor values
72 * @param strain_ptr Strain tensor values
73 * @param o_ptr Output objective function values
74 * @return MoFEMErrorCode Success or error code
75 */
76 MoFEMErrorCode evalInteriorObjectiveFunction(
77 MatrixDouble &coords, boost::shared_ptr<MatrixDouble> u_ptr,
78 boost::shared_ptr<MatrixDouble> stress_ptr,
79 boost::shared_ptr<MatrixDouble> strain_ptr,
80 boost::shared_ptr<MatrixDouble> o_ptr, bool symmetrize = true);
81
82 /**
83 * @brief Compute gradient of objective function with respect to stress
84 *
85 * Evaluates ∂f/∂σ where f is objective function and σ is stress tensor.
86 * This gradient is used in the adjoint method to compute sensitivities
87 * efficiently. Essential for gradient-based topology optimization.
88 *
89 * @param coords Gauss point coordinates
90 * @param u_ptr Displacement field values
91 * @param stress_ptr Stress tensor values
92 * @param strain_ptr Strain tensor values
93 * @param o_ptr Output gradient values
94 * @return MoFEMErrorCode Success or error code
95 */
96 MoFEMErrorCode evalInteriorObjectiveGradientStress(
97 MatrixDouble &coords, boost::shared_ptr<MatrixDouble> u_ptr,
98 boost::shared_ptr<MatrixDouble> stress_ptr,
99 boost::shared_ptr<MatrixDouble> strain_ptr,
100 boost::shared_ptr<MatrixDouble> o_ptr, bool symmetrize = true);
101
102 /**
103 * @brief Compute gradient of objective function with respect to strain
104 *
105 * Evaluates ∂f/∂ε where f is objective function and ε is strain tensor.
106 * Used when objective function depends directly on strain measures,
107 * complementing stress-based gradients in adjoint sensitivity analysis.
108 *
109 * @param coords Gauss point coordinates
110 * @param u_ptr Displacement field values
111 * @param stress_ptr Stress tensor values
112 * @param strain_ptr Strain tensor values
113 * @param o_ptr Output gradient values
114 * @return MoFEMErrorCode Success or error code
115 */
116 MoFEMErrorCode evalInteriorObjectiveGradientStrain(
117 MatrixDouble &coords, boost::shared_ptr<MatrixDouble> u_ptr,
118 boost::shared_ptr<MatrixDouble> stress_ptr,
119 boost::shared_ptr<MatrixDouble> strain_ptr,
120 boost::shared_ptr<MatrixDouble> o_ptr, bool symmetrize = true);
121
122 /**
123 * @brief Compute gradient of objective function with respect to displacement
124 *
125 * Evaluates ∂f/∂u where f is objective function and u is displacement field.
126 * This provides direct sensitivity of objective to displacement changes,
127 * used as right-hand side in adjoint equation K^T * λ = ∂f/∂u.
128 *
129 * @param coords Gauss point coordinates
130 * @param u_ptr Displacement field values
131 * @param stress_ptr Stress tensor values
132 * @param strain_ptr Strain tensor values
133 * @param o_ptr Output gradient values
134 * @return MoFEMErrorCode Success or error code
135 */
136 MoFEMErrorCode evalInteriorObjectiveGradientU(
137 MatrixDouble &coords, boost::shared_ptr<MatrixDouble> u_ptr,
138 boost::shared_ptr<MatrixDouble> stress_ptr,
139 boost::shared_ptr<MatrixDouble> strain_ptr,
140 boost::shared_ptr<MatrixDouble> o_ptr, bool symmetrize = true);
141
143 evalBoundaryObjectiveFunction(MatrixDouble &coords,
144 boost::shared_ptr<MatrixDouble> u_ptr,
145 boost::shared_ptr<MatrixDouble> t_ptr,
146 boost::shared_ptr<VectorDouble> o_ptr,
147 bool symmetrize = true);
148
150 evalBoundaryObjectiveGradientTraction(MatrixDouble &coords,
151 boost::shared_ptr<MatrixDouble> u_ptr,
152 boost::shared_ptr<MatrixDouble> t_ptr,
153 boost::shared_ptr<MatrixDouble> o_ptr);
154
156 evalBoundaryObjectiveGradientU(MatrixDouble &coords,
157 boost::shared_ptr<MatrixDouble> u_ptr,
158 boost::shared_ptr<MatrixDouble> t_ptr,
159 boost::shared_ptr<MatrixDouble> o_ptr);
160
161 /// Return number of topology optimization modes for given material block
162 MoFEMErrorCode numberOfModes(int block_id, int &modes);
163
164 /**
165 * @brief Define spatial topology modes for design optimization
166 *
167 * Generates basis functions that define how the geometry can be modified
168 * during topology optimization. These modes serve as design variables
169 * and define the design space for optimization.
170 *
171 * @param block_id Material block identifier
172 * @param coords Element coordinates
173 * @param centroid Block centroid coordinates
174 * @param bbodx Bounding box dimensions [xmin,xmax,ymin,ymax,zmin,zmax]
175 * @param o_ptr Output mode vectors
176 * @return MoFEMErrorCode Success or error code
177 */
178 MoFEMErrorCode blockModes(int block_id, MatrixDouble &coords,
179 std::array<double, 3> &centroid,
180 std::array<double, 6> &bbodx, MatrixDouble &o_ptr);
181
182private:
183 // Python interpreter objects for objective function evaluation
184 bp::object mainNamespace; ///< Main Python namespace for script execution
185
186 /**
187 * @brief Internal implementation for objective function evaluation
188 *
189 * Handles low-level Python function call with NumPy array conversion.
190 * Converts MoFEM matrices to NumPy arrays, calls Python function,
191 * and handles return value conversion.
192 *
193 * @param coords NumPy array of coordinates
194 * @param u NumPy array of displacements
195 * @param stress NumPy array of stress tensors
196 * @param strain NumPy array of strain tensors
197 * @param o Output NumPy array for objective values
198 * @return MoFEMErrorCode Success or error code
199 */
200 MoFEMErrorCode interiorObjectiveFunctionImpl(
201
202 np::ndarray coords, np::ndarray u,
203
204 np::ndarray stress, np::ndarray strain, np::ndarray &o
205
206 );
207
208 /**
209 * @brief Internal implementation for stress gradient computation
210 *
211 * Calls Python function to compute ∂f/∂σ with automatic array conversion.
212 * Essential for adjoint-based sensitivity analysis in topology optimization.
213 *
214 * @param coords NumPy array of coordinates
215 * @param u NumPy array of displacements
216 * @param stress NumPy array of stress tensors
217 * @param strain NumPy array of strain tensors
218 * @param o Output NumPy array for gradient values
219 * @return MoFEMErrorCode Success or error code
220 */
221 MoFEMErrorCode interiorObjectiveGradientStressImpl(
222
223 np::ndarray coords, np::ndarray u,
224
225 np::ndarray stress, np::ndarray strain, np::ndarray &o
226
227 );
228
229 /**
230 * @brief Internal implementation for strain gradient computation
231 *
232 * Evaluates ∂f/∂ε through Python interface with NumPy array handling.
233 * Provides strain-based sensitivities for comprehensive gradient computation.
234 *
235 * @param coords NumPy array of coordinates
236 * @param u NumPy array of displacements
237 * @param stress NumPy array of stress tensors
238 * @param strain NumPy array of strain tensors
239 * @param o Output NumPy array for gradient values
240 * @return MoFEMErrorCode Success or error code
241 */
242 MoFEMErrorCode interiorObjectiveGradientStrainImpl(
243
244 np::ndarray coords, np::ndarray u,
245
246 np::ndarray stress, np::ndarray strain, np::ndarray &o
247
248 );
249
250 /**
251 * @brief Internal implementation for displacement gradient computation
252 *
253 * Computes ∂f/∂u through Python interface for adjoint equation right-hand side.
254 * This gradient drives the adjoint solution that enables efficient sensitivity
255 * computation independent of design variable count.
256 *
257 * @param coords NumPy array of coordinates
258 * @param u NumPy array of displacements
259 * @param stress NumPy array of stress tensors
260 * @param strain NumPy array of strain tensors
261 * @param o Output NumPy array for gradient values
262 * @return MoFEMErrorCode Success or error code
263 */
264 MoFEMErrorCode interiorObjectiveGradientUImpl(
265
266 np::ndarray coords, np::ndarray u,
267
268 np::ndarray stress, np::ndarray strain, np::ndarray &o
269
270 );
271
272 MoFEMErrorCode boundaryObjectiveGradientTractionImpl(
273
274 np::ndarray coords, np::ndarray u,
275
276 np::ndarray t, np::ndarray &o
277
278 );
279
280 MoFEMErrorCode boundaryObjectiveFunctionImpl(
281
282 np::ndarray coords, np::ndarray u,
283
284 np::ndarray t, np::ndarray &o
285
286 );
287
288 MoFEMErrorCode boundaryObjectiveGradientUImpl(
289
290 np::ndarray coords, np::ndarray u,
291
292 np::ndarray t, np::ndarray &o
293
294 );
295
296 /**
297 * @brief Internal implementation for topology mode generation
298 *
299 * Calls Python function to generate spatial basis functions for topology
300 * optimization. These modes define the design space and parametrize
301 * allowable geometry modifications during optimization.
302 *
303 * @param block_id Material block identifier
304 * @param coords NumPy array of element coordinates
305 * @param centroid NumPy array of block centroid
306 * @param bbodx NumPy array of bounding box dimensions
307 * @param o_ptr Output NumPy array for mode vectors
308 * @return MoFEMErrorCode Success or error code
309 */
310 MoFEMErrorCode blockModesImpl(
311
312 int block_id, np::ndarray coords, np::ndarray centroid, np::ndarray bbodx,
313 np::ndarray &o_ptr
314
315 );
316
317 /**
318 * @brief Convert std::vector to NumPy array for Python interface
319 *
320 * Efficient conversion from MoFEM data structures to NumPy arrays
321 * for seamless Python function calls without data copying.
322 *
323 * @param data Source vector data
324 * @param rows Number of rows in resulting array
325 * @param nb_gauss_pts Number of Gauss points (affects array structure)
326 * @return np::ndarray NumPy array for Python use
327 */
328 np::ndarray convertToNumPy(std::vector<double> &data, int rows,
329 int nb_gauss_pts);
330
331 /**
332 * @brief Convert raw pointer to NumPy array for Python interface
333 *
334 * Low-level conversion for direct memory access to create NumPy arrays.
335 * Provides zero-copy conversion when possible for performance.
336 *
337 * @param ptr Raw data pointer
338 * @param s Array size
339 * @return np::ndarray NumPy array for Python use
340 */
341 np::ndarray convertToNumPy(double *ptr, int s);
342
343 /// Convert symmetric tensor storage to full matrix format
344 MatrixDouble copyToFull(MatrixDouble &s);
345
346 /// Convert full matrix to symmetric tensor storage format
347 void copyToSymmetric(double *ptr, MatrixDouble &s);
348};
349
350/**
351 * @brief Factory function to create Python-integrated objective function interface
352 *
353 * Creates and initializes an ObjectiveFunctionDataImpl instance that bridges
354 * MoFEM finite element computations with Python-defined objective functions.
355 * This enables flexible objective function definition for topology optimization
356 * while maintaining computational efficiency.
357 *
358 * The Python file must define specific functions with correct signatures:
359 * - objectiveInteriorFunction(coords, u, stress, strain) -> objective_value
360 * - objectiveInteriorGradientStress(coords, u, stress, strain) -> gradient_array
361 * - objectiveInteriorGradientStrain(coords, u, stress, strain) -> gradient_array
362 * - objectiveInteriorGradientU(coords, u, stress, strain) -> gradient_array
363 * - numberOfModes(block_id) -> integer
364 * - blockModes(block_id, coords, centroid, bbox) -> mode_array
365 *
366 * @param py_file Path to Python file containing objective function definitions
367 * @return boost::shared_ptr<ObjectiveFunctionData> Configured objective function interface
368 * @throws MoFEM exception if Python initialization fails
369 */
370boost::shared_ptr<ObjectiveFunctionData>
371create_python_objective_function(std::string py_file) {
372 auto ptr = boost::make_shared<ObjectiveFunctionDataImpl>();
373 CHK_THROW_MESSAGE(ptr->initPython(py_file), "init python");
374 return ptr;
375}
376
377/**
378 * @brief Initialize Python interpreter and load objective function script
379 *
380 * This method sets up the Python environment for objective function evaluation
381 * by loading a Python script that defines the required optimization functions.
382 * It establishes the bridge between MoFEM's C++ finite element computations
383 * and user-defined Python objective functions.
384 *
385 * The Python script must define the following functions:
386 * - f(coords, u, stress, strain): Main objective function
387 * - f_stress(coords, u, stress, strain): Gradient w.r.t. stress ∂f/∂σ
388 * - f_strain(coords, u, stress, strain): Gradient w.r.t. strain ∂f/∂ε
389 * - f_u(coords, u, stress, strain): Gradient w.r.t. displacement ∂f/∂u
390 * - number_of_modes(block_id): Return number of topology modes
391 * - block_modes(block_id, coords, centroid, bbox): Define topology modes
392 *
393 * All functions receive NumPy arrays and must return NumPy arrays of
394 * appropriate dimensions for the finite element computation.
395 *
396 * @param py_file Path to Python script containing objective function definitions
397 * @return MoFEMErrorCode Success or error code
398 * @throws MOFEM_OPERATION_UNSUCCESSFUL if Python script has errors
399 */
401ObjectiveFunctionDataImpl::initPython(const std::string py_file) {
403 try {
404
405 // Create main Python module and namespace for script execution
406 auto main_module = bp::import("__main__");
407 mainNamespace = main_module.attr("__dict__");
408
409 // Execute the Python script in the main namespace
410 bp::exec_file(py_file.c_str(), mainNamespace, mainNamespace);
411
412 // Python callbacks are resolved lazily with explicit existence checks
413
414 } catch (bp::error_already_set const &) {
415 // Handle Python errors by printing to stderr and throwing MoFEM exception
416 PyErr_Print();
418 }
420}
421
422MatrixDouble ObjectiveFunctionDataImpl::copyToFull(MatrixDouble &s) {
423 const auto nb_gauss_pts = s.size1();
424 MatrixDouble f(nb_gauss_pts, 9);
425 f.clear();
428 auto t_f =
430 f);
431 auto t_s = getFTensor2SymmetricFromMat<SPACE_DIM>(s);
432 for (size_t ii = 0; ii != nb_gauss_pts; ++ii) {
433 t_f(i, j) = t_s(i, j);
434 ++t_f;
435 ++t_s;
436 }
437 return f;
438};
439
440void ObjectiveFunctionDataImpl::copyToSymmetric(double *ptr, MatrixDouble &s) {
441 const auto nb_gauss_pts = s.size1();
444 auto t_f =
446 ptr);
447
448 auto t_s = getFTensor2SymmetricFromMat<SPACE_DIM>(s);
449 for (size_t ii = 0; ii != nb_gauss_pts; ++ii) {
450 t_s(i, j) = (t_f(i, j) || t_f(j, i)) / 2.0;
451 ++t_f;
452 ++t_s;
453 }
454}
455
456/**
457 * @brief Evaluate objective function at current finite element state
458 *
459 * This method bridges MoFEM finite element data with Python-defined objective
460 * functions for topology optimization. It handles the complete data conversion
461 * workflow from MoFEM matrices to NumPy arrays, calls the Python objective
462 * function, and converts results back to MoFEM format.
463 *
464 * Process:
465 * 1. Convert coordinate matrix to NumPy format for Python access
466 * 2. Convert displacement field data to NumPy arrays
467 * 3. Convert symmetric stress/strain tensors to full 3x3 matrix format
468 * 4. Call Python objective function: f(coords, u, stress, strain)
469 * 5. Extract results and copy back to MoFEM vector format
470 *
471 * The objective function typically computes scalar quantities like:
472 * - Compliance: ∫ u^T * f dΩ (minimize structural deformation)
473 * - Stress constraints: ∫ ||σ - σ_target||² dΩ (control stress distribution)
474 * - Volume constraints: ∫ ρ dΩ (material usage limitations)
475 *
476 * @param coords Gauss point coordinates for current element
477 * @param u_ptr Displacement field values at Gauss points
478 * @param stress_ptr Cauchy stress tensor values (symmetric storage)
479 * @param strain_ptr Strain tensor values (symmetric storage)
480 * @param o_ptr Output objective function values at each Gauss point
481 * @return MoFEMErrorCode Success or error code
482 */
483MoFEMErrorCode ObjectiveFunctionDataImpl::evalInteriorObjectiveFunction(
484 MatrixDouble &coords, boost::shared_ptr<MatrixDouble> u_ptr,
485 boost::shared_ptr<MatrixDouble> stress_ptr,
486 boost::shared_ptr<MatrixDouble> strain_ptr,
487 boost::shared_ptr<MatrixDouble> o_ptr, bool symmetrize) {
489 try {
490
491 // Convert coordinates to NumPy array for Python function
492 auto np_coords =
493 convertToNumPy(coords.data(), coords.size1(), coords.size2());
494 // Convert displacement field to NumPy array
495 auto np_u = convertToNumPy(u_ptr->data(), u_ptr->size1(), u_ptr->size2());
496
497 // Convert symmetric tensor storage to full matrix format for Python
498 // MoFEM stores symmetric tensors in like Voigt notation, Python expects full 3x3 matrices
499 auto full_stress = symmetrize ? copyToFull(*(stress_ptr)) : *(stress_ptr);
500 auto full_strain = symmetrize ? copyToFull(*(strain_ptr)) : *(strain_ptr);
501
502 // Create NumPy arrays for stress and strain tensors
503 auto np_stress = convertToNumPy(full_stress.data(), full_stress.size1(),
504 full_stress.size2());
505 auto np_strain = convertToNumPy(full_strain.data(), full_strain.size1(),
506 full_strain.size2());
507
508 // Prepare output array for objective function values
509 const auto nb_gauss_pts = full_strain.size1();
510 np::ndarray np_output =
511 np::empty(bp::make_tuple(nb_gauss_pts), np::dtype::get_builtin<double>());
512
513 // Call Python objective function implementation
514 CHKERR interiorObjectiveFunctionImpl(np_coords, np_u, np_stress, np_strain,
515 np_output);
516
517 //Check the shape of returned array
518 if (np_output.get_nd() != 1 || np_output.get_shape()[0] != nb_gauss_pts) {
521 "Wrong shape of Objective Function from python expected (" +
522 std::to_string(nb_gauss_pts) + "), got (" +
523 std::to_string(np_output.get_shape()[0]) + ")");
524 }
525
526 // Copy Python results back to a 1 x n matrix, matching common-data storage.
527 o_ptr->resize(1, nb_gauss_pts, false);
528 double *val_ptr = reinterpret_cast<double *>(np_output.get_data());
529 std::copy(val_ptr, val_ptr + nb_gauss_pts, o_ptr->data().begin());
530
531 } catch (bp::error_already_set const &) {
532 // Handle Python errors with detailed error reporting
533 PyErr_Print();
535 }
537}
538
539/**
540 * @brief Compute gradient of objective function with respect to stress tensor
541 *
542 * This method evaluates ∂f/∂σ, the partial derivative of the objective function
543 * with respect to the Cauchy stress tensor. This gradient is fundamental to the
544 * adjoint method for topology optimization, as it provides the driving force
545 * for the adjoint equation solution.
546 *
547 * Mathematical context:
548 * The adjoint method requires ∂f/∂σ to compute sensitivities efficiently.
549 * For stress-based objectives like von Mises stress constraints:
550 * ∂f/∂σ = ∂/∂σ[∫(σ_vm - σ_target)² dΩ] = 2(σ_vm - σ_target) * ∂σ_vm/∂σ
551 *
552 * Process:
553 * 1. Convert all field data to NumPy format for Python processing
554 * 2. Call Python function f_stress(coords, u, stress, strain)
555 * 3. Python returns full 3x3 gradient matrices for each Gauss point
556 * 4. Convert back to symmetric tensor storage used by MoFEM
557 * 5. Store results for use in adjoint equation assembly
558 *
559 * The resulting gradients drive the adjoint solution that enables efficient
560 * computation of design sensitivities independent of design variable count.
561 *
562 * @param coords Gauss point coordinates
563 * @param u_ptr Displacement field values
564 * @param stress_ptr Current stress tensor values (symmetric storage)
565 * @param strain_ptr Current strain tensor values (symmetric storage)
566 * @param o_ptr Output stress gradients ∂f/∂σ (symmetric storage)
567 * @return MoFEMErrorCode Success or error code
568 */
569MoFEMErrorCode ObjectiveFunctionDataImpl::evalInteriorObjectiveGradientStress(
570 MatrixDouble &coords, boost::shared_ptr<MatrixDouble> u_ptr,
571 boost::shared_ptr<MatrixDouble> stress_ptr,
572 boost::shared_ptr<MatrixDouble> strain_ptr,
573 boost::shared_ptr<MatrixDouble> o_ptr, bool symmetrize) {
575 try {
576
577 // Convert coordinates and displacement field to NumPy format
578 auto np_coords =
579 convertToNumPy(coords.data(), coords.size1(), coords.size2());
580 auto np_u = convertToNumPy(u_ptr->data(), u_ptr->size1(), u_ptr->size2());
581
582 // Convert symmetric tensors to full 3x3 format for Python processing
583 auto full_stress = symmetrize ? copyToFull(*(stress_ptr)) : *(stress_ptr);
584 auto full_strain = symmetrize ? copyToFull(*(strain_ptr)) : *(strain_ptr);
585
586 // Create NumPy arrays for stress and strain tensors
587 auto np_stress = convertToNumPy(full_stress.data(), full_stress.size1(),
588 full_stress.size2());
589 auto np_strain = convertToNumPy(full_strain.data(), full_strain.size1(),
590 full_strain.size2());
591 // Prepare output array for stress gradients (full matrix format)
592 np::ndarray np_output =
593 np::empty(bp::make_tuple(full_strain.size1(), full_strain.size2()),
594 np::dtype::get_builtin<double>());
595
596 // Call Python implementation for stress gradient computation
597 CHKERR interiorObjectiveGradientStressImpl(np_coords, np_u, np_stress, np_strain,
598 np_output);
599
600 // Check the shape of returned array
601 if (np_output.get_shape()[0] != full_strain.size1() ||
602 np_output.get_shape()[1] != full_strain.size2()) {
605 "Wrong shape of Objective Gradient from python expected (" +
606 std::to_string(full_strain.size1()) + ", " +
607 std::to_string(full_strain.size2()) + "), got (" +
608 std::to_string(np_output.get_shape()[0]) + ", " +
609 std::to_string(np_output.get_shape()[1]) + ")");
610 }
611
612 // Prepare output matrix in the requested tensor storage format
613 o_ptr->resize(stress_ptr->size1(), stress_ptr->size2(), false);
614 if (symmetrize) {
615 double *val_ptr = reinterpret_cast<double *>(np_output.get_data());
616 // Convert full matrix results back to symmetric tensor storage
617 copyToSymmetric(val_ptr, *(o_ptr));
618 } else {
619 double *val_ptr = reinterpret_cast<double *>(np_output.get_data());
620 std::copy(val_ptr, val_ptr + stress_ptr->size1() * stress_ptr->size2(),
621 o_ptr->data().begin());
622 }
623
624 } catch (bp::error_already_set const &) {
625 PyErr_Print();
627 }
629}
630
631/**
632 * @brief Compute gradient of objective function with respect to strain tensor
633 *
634 * This method evaluates ∂f/∂ε, the partial derivative of the objective function
635 * with respect to the strain tensor. While many structural objectives depend
636 * primarily on stress, strain-based gradients are important for certain
637 * optimization formulations and provide additional sensitivity information.
638 *
639 * Mathematical context:
640 * For strain energy-based objectives: f = ½ε:C:ε
641 * The gradient is: ∂f/∂ε = C:ε = σ (stress tensor)
642 *
643 * For strain-based constraints or objectives like strain concentration:
644 * ∂f/∂ε = ∂/∂ε[∫(ε_vm - ε_target)² dΩ] = 2(ε_vm - ε_target) * ∂ε_vm/∂ε
645 *
646 * Process:
647 * 1. Convert field data to NumPy format for Python compatibility
648 * 2. Call Python function f_strain(coords, u, stress, strain)
649 * 3. Python returns gradient matrices ∂f/∂ε for each Gauss point
650 * 4. Convert from full 3x3 format back to symmetric storage
651 * 5. Results used in adjoint sensitivity analysis
652 *
653 * This gradient complements stress-based gradients in comprehensive
654 * sensitivity analysis for topology optimization problems.
655 *
656 * @param coords Gauss point coordinates
657 * @param u_ptr Displacement field values
658 * @param stress_ptr Current stress tensor values (symmetric storage)
659 * @param strain_ptr Current strain tensor values (symmetric storage)
660 * @param o_ptr Output strain gradients ∂f/∂ε (symmetric storage)
661 * @return MoFEMErrorCode Success or error code
662 */
663MoFEMErrorCode ObjectiveFunctionDataImpl::evalInteriorObjectiveGradientStrain(
664 MatrixDouble &coords, boost::shared_ptr<MatrixDouble> u_ptr,
665 boost::shared_ptr<MatrixDouble> stress_ptr,
666 boost::shared_ptr<MatrixDouble> strain_ptr,
667 boost::shared_ptr<MatrixDouble> o_ptr, bool symmetrize) {
669 try {
670
671 // Convert coordinates and displacement data to NumPy format
672 auto np_coords =
673 convertToNumPy(coords.data(), coords.size1(), coords.size2());
674 auto np_u = convertToNumPy(u_ptr->data(), u_ptr->size1(), u_ptr->size2());
675
676 // Convert symmetric tensor data to full 3x3 matrices for Python
677 auto full_stress = symmetrize ? copyToFull(*(stress_ptr)) : *(stress_ptr);
678 auto full_strain = symmetrize ? copyToFull(*(strain_ptr)) : *(strain_ptr);
679
680 auto np_stress = convertToNumPy(full_stress.data(), full_stress.size1(),
681 full_stress.size2());
682 auto np_strain = convertToNumPy(full_strain.data(), full_strain.size1(),
683 full_strain.size2());
684
685 // Prepare output array for strain gradients
686 np::ndarray np_output =
687 np::empty(bp::make_tuple(full_strain.size1(), full_strain.size2()),
688 np::dtype::get_builtin<double>());
689
690 // Call Python implementation for strain gradient computation
691 CHKERR interiorObjectiveGradientStrainImpl(np_coords, np_u, np_stress, np_strain,
692 np_output);
693
694 // Check the shape of returned array
695 if (np_output.get_shape()[0] != full_strain.size1() ||
696 np_output.get_shape()[1] != full_strain.size2()) {
699 "Wrong shape of Objective Gradient from python expected (" +
700 std::to_string(full_strain.size1()) + ", " +
701 std::to_string(full_strain.size2()) + "), got (" +
702 std::to_string(np_output.get_shape()[0]) + ", " +
703 std::to_string(np_output.get_shape()[1]) + ")");
704 }
705
706 o_ptr->resize(strain_ptr->size1(), strain_ptr->size2(), false);
707 if (symmetrize) {
708 double *val_ptr = reinterpret_cast<double *>(np_output.get_data());
709 copyToSymmetric(val_ptr, *(o_ptr));
710 } else {
711 double *val_ptr = reinterpret_cast<double *>(np_output.get_data());
712 std::copy(val_ptr, val_ptr + strain_ptr->size1() * strain_ptr->size2(),
713 o_ptr->data().begin());
714 }
715
716 } catch (bp::error_already_set const &) {
717 PyErr_Print();
719 }
721}
722
723/**
724 * @brief Compute gradient of objective function with respect to displacement field
725 *
726 * This method evaluates ∂f/∂u, the partial derivative of the objective function
727 * with respect to the displacement field. This gradient is crucial for the adjoint
728 * method as it forms the right-hand side of the adjoint equation: K^T * λ = ∂f/∂u
729 *
730 * Mathematical context:
731 * The adjoint method solves: K^T * λ = ∂f/∂u
732 * where λ are the adjoint variables (Lagrange multipliers)
733 *
734 * For compliance minimization: f = ½u^T * K * u
735 * The gradient is: ∂f/∂u = K * u (applied forces)
736 *
737 * For displacement-based constraints: f = ||u - u_target||²
738 * The gradient is: ∂f/∂u = 2(u - u_target)
739 *
740 * Process:
741 * 1. Convert all field data to NumPy format for Python processing
742 * 2. Call Python function f_u(coords, u, stress, strain)
743 * 3. Python returns displacement gradients for each component
744 * 4. Copy results directly (no tensor conversion needed for vectors)
745 * 5. Results drive adjoint equation solution for sensitivity analysis
746 *
747 * This gradient is fundamental to adjoint-based topology optimization,
748 * enabling efficient sensitivity computation for any number of design variables.
749 *
750 * @param coords Gauss point coordinates
751 * @param u_ptr Displacement field values
752 * @param stress_ptr Current stress tensor values
753 * @param strain_ptr Current strain tensor values
754 * @param o_ptr Output displacement gradients ∂f/∂u
755 * @return MoFEMErrorCode Success or error code
756 */
757MoFEMErrorCode ObjectiveFunctionDataImpl::evalInteriorObjectiveGradientU(
758 MatrixDouble &coords, boost::shared_ptr<MatrixDouble> u_ptr,
759 boost::shared_ptr<MatrixDouble> stress_ptr,
760 boost::shared_ptr<MatrixDouble> strain_ptr,
761 boost::shared_ptr<MatrixDouble> o_ptr, bool symmetrize) {
763 try {
764
765 // Convert coordinates and displacement field to NumPy format
766 auto np_coords =
767 convertToNumPy(coords.data(), coords.size1(), coords.size2());
768 auto np_u = convertToNumPy(u_ptr->data(), u_ptr->size1(), u_ptr->size2());
769
770 // Convert stress and strain tensors to full matrix format
771 auto full_stress = symmetrize ? copyToFull(*(stress_ptr)) : *(stress_ptr);
772 auto full_strain = symmetrize ? copyToFull(*(strain_ptr)) : *(strain_ptr);
773
774 auto np_stress = convertToNumPy(full_stress.data(), full_stress.size1(),
775 full_stress.size2());
776 auto np_strain = convertToNumPy(full_strain.data(), full_strain.size1(),
777 full_strain.size2());
778
779 // Prepare output array for displacement gradients (same size as displacement field)
780 np::ndarray np_output =
781 np::empty(bp::make_tuple(u_ptr->size1(), u_ptr->size2()),
782 np::dtype::get_builtin<double>());
783
784 // Call Python implementation for displacement gradient computation
785 // Note: This should call interiorObjectiveGradientUImpl, not interiorObjectiveGradientStrainImpl
786 CHKERR interiorObjectiveGradientUImpl(np_coords, np_u, np_stress, np_strain,
787 np_output);
788
789 // Check the shape of returned array
790 if (np_output.get_shape()[0] != u_ptr->size1() ||
791 np_output.get_shape()[1] != u_ptr->size2()) {
794 "Wrong shape of Objective Gradient from python expected (" +
795 std::to_string(u_ptr->size1()) + ", " +
796 std::to_string(u_ptr->size2()) + "), got (" +
797 std::to_string(np_output.get_shape()[0]) + ", " +
798 std::to_string(np_output.get_shape()[1]) + ")");
799 }
800
801 // Copy results directly to output matrix (no tensor conversion needed for vectors)
802 o_ptr->resize(u_ptr->size1(), u_ptr->size2(), false);
803 double *val_ptr = reinterpret_cast<double *>(np_output.get_data());
804 std::copy(val_ptr, val_ptr + u_ptr->size1() * u_ptr->size2(),
805 o_ptr->data().begin());
806
807 } catch (bp::error_already_set const &) {
808 // Handle Python errors with detailed reporting
809 PyErr_Print();
811 }
813}
814
815MoFEMErrorCode ObjectiveFunctionDataImpl::evalBoundaryObjectiveGradientTraction(
816 MatrixDouble &coords, boost::shared_ptr<MatrixDouble> u_ptr,
817 boost::shared_ptr<MatrixDouble> t_ptr,
818 boost::shared_ptr<MatrixDouble> o_ptr) {
820 try {
821
822 auto np_coords =
823 convertToNumPy(coords.data(), coords.size1(), coords.size2());
824 auto np_u = convertToNumPy(u_ptr->data(), u_ptr->size1(), u_ptr->size2());
825 auto np_t = convertToNumPy(t_ptr->data(), t_ptr->size1(), t_ptr->size2());
826
827 np::ndarray np_output =
828 np::empty(bp::make_tuple(u_ptr->size1(), u_ptr->size2()),
829 np::dtype::get_builtin<double>());
830
831 CHKERR boundaryObjectiveGradientTractionImpl(np_coords, np_u, np_t, np_output);
832
833 // Check the shape of returned array
834 if (np_output.get_shape()[0] != u_ptr->size1() ||
835 np_output.get_shape()[1] != u_ptr->size2()) {
838 "Wrong shape of Objective Gradient from python expected (" +
839 std::to_string(u_ptr->size1()) + ", " +
840 std::to_string(u_ptr->size2()) + "), got (" +
841 std::to_string(np_output.get_shape()[0]) + ", " +
842 std::to_string(np_output.get_shape()[1]) + ")");
843 }
844
845 o_ptr->resize(u_ptr->size1(), u_ptr->size2(), false);
846 double *val_ptr = reinterpret_cast<double *>(np_output.get_data());
847 std::copy(val_ptr, val_ptr + u_ptr->size1() * u_ptr->size2(),
848 o_ptr->data().begin());
849
850 } catch (bp::error_already_set const &) {
851 PyErr_Print();
853 }
855}
856
857MoFEMErrorCode ObjectiveFunctionDataImpl::evalBoundaryObjectiveFunction(
858 MatrixDouble &coords, boost::shared_ptr<MatrixDouble> u_ptr,
859 boost::shared_ptr<MatrixDouble> t_ptr,
860 boost::shared_ptr<VectorDouble> o_ptr, bool symmetrize) {
862 try {
863 (void)symmetrize;
864
865 auto np_coords =
866 convertToNumPy(coords.data(), coords.size1(), coords.size2());
867 auto np_u = convertToNumPy(u_ptr->data(), u_ptr->size1(), u_ptr->size2());
868 auto np_t = convertToNumPy(t_ptr->data(), t_ptr->size1(), t_ptr->size2());
869
870 np::ndarray np_output = np::empty(bp::make_tuple(t_ptr->size2()),
871 np::dtype::get_builtin<double>());
872
873 CHKERR boundaryObjectiveFunctionImpl(np_coords, np_u, np_t, np_output);
874
875 // Check the shape of returned array
876 if (np_output.get_nd() != 1 || np_output.get_shape()[0] != t_ptr->size2()) {
879 "Wrong shape of Objective Function from python expected (" +
880 std::to_string(t_ptr->size2()) + "), got (" +
881 std::to_string(np_output.get_shape()[0]) + ")");
882 }
883
884 o_ptr->resize(t_ptr->size2(), false);
885 double *val_ptr = reinterpret_cast<double *>(np_output.get_data());
886 std::copy(val_ptr, val_ptr + t_ptr->size2(), o_ptr->data().begin());
887
888 } catch (bp::error_already_set const &) {
889 PyErr_Print();
891 }
893}
894
895MoFEMErrorCode ObjectiveFunctionDataImpl::evalBoundaryObjectiveGradientU(
896 MatrixDouble &coords, boost::shared_ptr<MatrixDouble> u_ptr,
897 boost::shared_ptr<MatrixDouble> t_ptr,
898 boost::shared_ptr<MatrixDouble> o_ptr) {
900 try {
901 auto np_coords =
902 convertToNumPy(coords.data(), coords.size1(), coords.size2());
903 auto np_u = convertToNumPy(u_ptr->data(), u_ptr->size1(), u_ptr->size2());
904 auto np_t = convertToNumPy(t_ptr->data(), t_ptr->size1(), t_ptr->size2());
905
906 np::ndarray np_output =
907 np::empty(bp::make_tuple(u_ptr->size1(), u_ptr->size2()),
908 np::dtype::get_builtin<double>());
909
910 CHKERR boundaryObjectiveGradientUImpl(np_coords, np_u, np_t, np_output);
911
912 // Check the shape of returned array
913 if (np_output.get_shape()[0] != u_ptr->size1() ||
914 np_output.get_shape()[1] != u_ptr->size2()) {
917 "Wrong shape of Objective Gradient from python expected (" +
918 std::to_string(u_ptr->size1()) + ", " +
919 std::to_string(u_ptr->size2()) + "), got (" +
920 std::to_string(np_output.get_shape()[0]) + ", " +
921 std::to_string(np_output.get_shape()[1]) + ")");
922 }
923
924 o_ptr->resize(u_ptr->size1(), u_ptr->size2(), false);
925 double *val_ptr = reinterpret_cast<double *>(np_output.get_data());
926 std::copy(val_ptr, val_ptr + u_ptr->size1() * u_ptr->size2(),
927 o_ptr->data().begin());
928
929 } catch (bp::error_already_set const &) {
930 PyErr_Print();
932 }
934}
935
936/**
937 * @brief Generate spatial topology modes for design optimization
938 *
939 * This method defines the design parameterization for topology optimization by
940 * generating spatial basis functions (modes) that describe how the geometry
941 * can be modified during optimization. These modes serve as design variables
942 * and define the feasible design space for the optimization problem.
943 *
944 * Mathematical context:
945 * The geometry modification is parameterized as: x_new = x_original + Σ(αᵢ * φᵢ(x))
946 * where αᵢ are design variables and φᵢ(x) are spatial mode functions
947 *
948 * Common mode types:
949 * - Radial basis functions: φ(x) = exp(-||x-c||²/σ²) for localized changes
950 * - Polynomial modes: φ(x) = xⁿyᵐzᵖ for global shape changes
951 * - Sinusoidal modes: φ(x) = sin(kx)cos(ly) for periodic patterns
952 * - Principal component modes: Derived from geometric sensitivity analysis
953 *
954 * Process:
955 * 1. Query Python function for number of modes for this material block
956 * 2. Convert coordinate data and geometric information to NumPy format
957 * 3. Call Python function block_modes(block_id, coords, centroid, bbox)
958 * 4. Python returns mode vectors for each coordinate at each mode
959 * 5. Reshape and store modes for use as design variables in optimization
960 *
961 * The modes enable efficient design space exploration and gradient-based
962 * optimization while maintaining geometric feasibility and smoothness.
963 *
964 * @param block_id Material block identifier for mode generation
965 * @param coords Element coordinates where modes are evaluated
966 * @param centroid Geometric centroid of the material block [x,y,z]
967 * @param bbodx Bounding box dimensions [xmin,xmax,ymin,ymax,zmin,zmax]
968 * @param o_ptr Output matrix: modes × (coordinates × spatial_dimension)
969 * @return MoFEMErrorCode Success or error code
970 */
971MoFEMErrorCode ObjectiveFunctionDataImpl::blockModes(
972 int block_id, MatrixDouble &coords, std::array<double, 3> &centroid,
973 std::array<double, 6> &bbodx, MatrixDouble &o_ptr) {
975 try {
976
977 // Query Python function for number of topology modes for this block
978 int nb_modes;
979 CHKERR numberOfModes(block_id, nb_modes);
980
981 // Convert coordinate matrix to NumPy format for Python processing
982 auto np_coords =
983 convertToNumPy(coords.data(), coords.size1(), coords.size2());
984
985 // Convert geometric information to NumPy arrays
986 auto np_centroid =
987 convertToNumPy(centroid.data(), 3); // Block centroid [x,y,z]
988 auto np_bbodx = convertToNumPy(
989 bbodx.data(), 6); // Bounding box [xmin,xmax,ymin,ymax,zmin,zmax]
990
991 // Prepare output array: [modes × (coordinates * spatial_dimensions)]
992 np::ndarray np_output =
993 np::empty(bp::make_tuple(nb_modes, coords.size1(), coords.size2()),
994 np::dtype::get_builtin<double>());
995
996 // Call Python implementation to generate topology modes
997 CHKERR blockModesImpl(block_id, np_coords, np_centroid, np_bbodx,
998 np_output);
999
1000 // Check the shape of returned array
1001 if (np_output.get_shape()[0] != nb_modes ||
1002 np_output.get_shape()[1] != coords.size1() ||
1003 np_output.get_shape()[2] != coords.size2()) {
1005 "Wrong shape of Modes from python expected (" +
1006 std::to_string(nb_modes) + ", " +
1007 std::to_string(coords.size1()) + ", " +
1008 std::to_string(coords.size2()) + "), got (" +
1009 std::to_string(np_output.get_shape()[0]) + ", " +
1010 std::to_string(np_output.get_shape()[1]) + ", " +
1011 std::to_string(np_output.get_shape()[2]) + ")");
1012 }
1013
1014 // Reshape output matrix for MoFEM format: [modes × (coordinates * spatial_dimensions)]
1015 o_ptr.resize(nb_modes, coords.size1() * coords.size2(), false);
1016 double *val_ptr = reinterpret_cast<double *>(np_output.get_data());
1017 // Copy flattened mode data to output matrix
1018 std::copy(val_ptr, val_ptr + coords.size1() * coords.size2() * nb_modes,
1019 o_ptr.data().begin());
1020
1021 } catch (bp::error_already_set const &) {
1022 // Handle Python errors in mode generation
1023 PyErr_Print();
1025 }
1027}
1028
1029MoFEMErrorCode ObjectiveFunctionDataImpl::interiorObjectiveFunctionImpl(
1030
1031 np::ndarray coords, np::ndarray u,
1032
1033 np::ndarray stress, np::ndarray strain, np::ndarray &o
1034
1035) {
1037 try {
1038
1039 if (bp::extract<bool>(mainNamespace.attr("__contains__")("f"))) {
1040 // Deprecated: Check for main objective function 'f' first for backward compatibility
1041 o = bp::extract<np::ndarray>(
1042 mainNamespace["f"](coords, u, stress, strain));
1043 } else if (bp::extract<bool>(
1044 mainNamespace.attr("__contains__")("f_interior"))) {
1045 o = bp::extract<np::ndarray>(
1046 mainNamespace["f_interior"](coords, u, stress, strain));
1047 } else {
1050 "Python function f_interior(coords,u,stress,strain) is not defined");
1051 }
1052
1053 } catch (bp::error_already_set const &) {
1054 // print all other errors to stderr
1055 PyErr_Print();
1057 }
1059}
1060
1061MoFEMErrorCode ObjectiveFunctionDataImpl::interiorObjectiveGradientStressImpl(
1062
1063 np::ndarray coords, np::ndarray u,
1064
1065 np::ndarray stress, np::ndarray strain, np::ndarray &o
1066
1067) {
1069 try {
1070
1071 if (bp::extract<bool>(mainNamespace.attr("__contains__")("f_stress"))) {
1072 // Deprecated: keep legacy name first for backward compatibility
1073 o = bp::extract<np::ndarray>(
1074 mainNamespace["f_stress"](coords, u, stress, strain));
1075 } else if (bp::extract<bool>(
1076 mainNamespace.attr("__contains__")("f_interior_stress"))) {
1077 o = bp::extract<np::ndarray>(
1078 mainNamespace["f_interior_stress"](coords, u, stress, strain));
1079 } else {
1082 "Python function f_interior_stress(coords,u,stress,strain) is not defined");
1083 }
1084
1085 } catch (bp::error_already_set const &) {
1086 // print all other errors to stderr
1087 PyErr_Print();
1089 }
1091}
1092
1093MoFEMErrorCode ObjectiveFunctionDataImpl::interiorObjectiveGradientStrainImpl(
1094
1095 np::ndarray coords, np::ndarray u,
1096
1097 np::ndarray stress, np::ndarray strain, np::ndarray &o
1098
1099) {
1101 try {
1102
1103 if (bp::extract<bool>(mainNamespace.attr("__contains__")("f_strain"))) {
1104 // Deprecated: keep legacy name first for backward compatibility
1105 o = bp::extract<np::ndarray>(
1106 mainNamespace["f_strain"](coords, u, stress, strain));
1107 } else if (bp::extract<bool>(
1108 mainNamespace.attr("__contains__")("f_interior_strain"))) {
1109 o = bp::extract<np::ndarray>(
1110 mainNamespace["f_interior_strain"](coords, u, stress, strain));
1111 } else {
1114 "Python function f_interior_strain(coords,u,stress,strain) is not defined");
1115 }
1116
1117 } catch (bp::error_already_set const &) {
1118 // print all other errors to stderr
1119 PyErr_Print();
1121 }
1123}
1124
1125MoFEMErrorCode ObjectiveFunctionDataImpl::interiorObjectiveGradientUImpl(
1126
1127 np::ndarray coords, np::ndarray u,
1128
1129 np::ndarray stress, np::ndarray strain, np::ndarray &o
1130
1131) {
1133 try {
1134
1135 if (bp::extract<bool>(mainNamespace.attr("__contains__")("f_u"))) {
1136 // Deprecated: keep legacy name first for backward compatibility
1137 o = bp::extract<np::ndarray>(
1138 mainNamespace["f_u"](coords, u, stress, strain));
1139 } else if (bp::extract<bool>(
1140 mainNamespace.attr("__contains__")("f_interior_u"))) {
1141 o = bp::extract<np::ndarray>(
1142 mainNamespace["f_interior_u"](coords, u, stress, strain));
1143 } else {
1146 "Python function f_interior_u(coords,u,stress,strain) is not defined");
1147 }
1148
1149 } catch (bp::error_already_set const &) {
1150 // print all other errors to stderr
1151 PyErr_Print();
1153 }
1155}
1156
1157MoFEMErrorCode ObjectiveFunctionDataImpl::boundaryObjectiveGradientTractionImpl(
1158
1159 np::ndarray coords, np::ndarray u,
1160
1161 np::ndarray t, np::ndarray &o
1162
1163) {
1165 try {
1166
1167 if (bp::extract<bool>(mainNamespace.attr("__contains__")("f_boundary_t"))) {
1168 o = bp::extract<np::ndarray>(mainNamespace["f_boundary_t"](coords, u, t));
1169 } else {
1172 "Python function f_boundary_t(coords,u,t) is not defined");
1173 }
1174
1175 } catch (bp::error_already_set const &) {
1176 PyErr_Print();
1178 }
1180}
1181
1182MoFEMErrorCode ObjectiveFunctionDataImpl::boundaryObjectiveFunctionImpl(
1183
1184 np::ndarray coords, np::ndarray u,
1185
1186 np::ndarray t, np::ndarray &o
1187
1188) {
1190 try {
1191
1192 if (bp::extract<bool>(mainNamespace.attr("__contains__")("f_boundary"))) {
1193 o = bp::extract<np::ndarray>(mainNamespace["f_boundary"](coords, u, t));
1194 } else if (bp::extract<bool>(
1195 mainNamespace.attr("__contains__")("f_boundary_function"))) {
1196 o = bp::extract<np::ndarray>(
1197 mainNamespace["f_boundary_function"](coords, u, t));
1198 } else {
1201 "Python function f_boundary(coords,u,t) is not defined");
1202 }
1203
1204 } catch (bp::error_already_set const &) {
1205 PyErr_Print();
1207 }
1209}
1210
1211MoFEMErrorCode ObjectiveFunctionDataImpl::boundaryObjectiveGradientUImpl(
1212
1213 np::ndarray coords, np::ndarray u,
1214
1215 np::ndarray t, np::ndarray &o
1216
1217) {
1219 try {
1220
1221 if (bp::extract<bool>(mainNamespace.attr("__contains__")("f_boundary_u"))) {
1222 o = bp::extract<np::ndarray>(mainNamespace["f_boundary_u"](coords, u, t));
1223 } else {
1226 "Python function f_boundary_u(coords,u,t) is not defined");
1227 }
1228
1229 } catch (bp::error_already_set const &) {
1230 PyErr_Print();
1232 }
1234}
1235
1236MoFEMErrorCode ObjectiveFunctionDataImpl::numberOfModes(int block_id,
1237 int &modes) {
1239 try {
1240
1241 if (bp::extract<bool>(
1242 mainNamespace.attr("__contains__")("number_of_modes"))) {
1243 modes = bp::extract<int>(mainNamespace["number_of_modes"](block_id));
1244 } else {
1247 "Python function number_of_modes(block_id) is not defined");
1248 }
1249
1250 } catch (bp::error_already_set const &) {
1251 // print all other errors to stderr
1252 PyErr_Print();
1254 }
1256}
1257
1258MoFEMErrorCode ObjectiveFunctionDataImpl::blockModesImpl(int block_id,
1259 np::ndarray coords,
1260 np::ndarray centroid,
1261 np::ndarray bbodx,
1262 np::ndarray &o) {
1264 try {
1265 if (bp::extract<bool>(mainNamespace.attr("__contains__")("block_modes"))) {
1266 o = bp::extract<np::ndarray>(
1267 mainNamespace["block_modes"](block_id, coords, centroid, bbodx));
1268 } else {
1271 "Python function block_modes(block_id,coords,centroid,bbox) is not "
1272 "defined");
1273 }
1274 } catch (bp::error_already_set const &) {
1275 // print all other errors to stderr
1276 PyErr_Print();
1278 }
1280}
1281
1282/**
1283 * @brief Converts a std::vector<double> to a NumPy ndarray.
1284 *
1285 * This function wraps the given vector data into a NumPy array with the
1286 * specified number of rows and Gauss points. The resulting ndarray shares
1287 * memory with the input vector, so changes to one will affect the other.
1288 *
1289 * @param data Reference to the vector containing double values to be converted.
1290 * @param rows Number of rows in the resulting NumPy array.
1291 * @param nb_gauss_pts Number of Gauss points (columns) in the resulting NumPy
1292 * array.
1293 * @return np::ndarray NumPy array view of the input data.
1294 *
1295 * @note
1296 * - `size` specifies the shape of the resulting ndarray as a tuple (rows,
1297 * nb_gauss_pts).
1298 * - `stride` specifies the step size in bytes to move to the next element in
1299 * memory. Here, it is set to sizeof(double), indicating contiguous storage for
1300 * each element.
1301 */
1302inline np::ndarray
1303ObjectiveFunctionDataImpl::convertToNumPy(std::vector<double> &data, int rows,
1304 int nb_gauss_pts) {
1305 auto dtype = np::dtype::get_builtin<double>();
1306 auto size = bp::make_tuple(rows, nb_gauss_pts);
1307 auto stride = bp::make_tuple(nb_gauss_pts * sizeof(double), sizeof(double));
1308 return (np::from_data(data.data(), dtype, size, stride, bp::object()));
1309}
1310
1311inline np::ndarray ObjectiveFunctionDataImpl::convertToNumPy(double *ptr,
1312 int s) {
1313 auto dtype = np::dtype::get_builtin<double>();
1314 auto size = bp::make_tuple(s);
1315 auto stride = bp::make_tuple(sizeof(double));
1316 return (np::from_data(ptr, dtype, size, stride, bp::object()));
1317}
1318
1319#else
1320
1321boost::shared_ptr<ObjectiveFunctionData>
1324 "Python objective function requires Boost.NumPy");
1325 return {};
1326}
1327
1328#endif // ENABLE_PYTHON_BINDING
1329
1330} // namespace ShapeOptimization
Interface for Python-based objective function evaluation in topology optimization.
#define FTENSOR_INDEX(DIM, I)
constexpr int SPACE_DIM
#define CHK_THROW_MESSAGE(err, msg)
Check and throw MoFEM exception.
#define MoFEMFunctionBegin
First executable line of each MoFEM function, used for error handling. Final line of MoFEM functions ...
@ MOFEM_OPERATION_UNSUCCESSFUL
Definition definitions.h:34
@ MOFEM_DATA_INCONSISTENCY
Definition definitions.h:31
@ MOFEM_NOT_IMPLEMENTED
Definition definitions.h:32
#define MoFEMFunctionReturn(a)
Last executable line of each PETSc function used for error handling. Replaces return()
#define CHKERR
Inline error check.
FTensor::Index< 'i', SPACE_DIM > i
FTensor::Index< 'j', 3 > j
PetscErrorCode MoFEMErrorCode
MoFEM/PETSc error code.
implementation of Data Operators for Forces and Sources
Definition Common.hpp:10
auto getFTensor2FromMat(M &data)
Get tensor rank 2 (matrix) form data matrix.
auto getFTensor2FromPtr(double *ptr)
boost::shared_ptr< ObjectiveFunctionData > create_python_objective_function(std::string)
constexpr double t
plate stiffness
Definition plate.cpp:58
Abstract interface for Python-defined objective functions.
#define EXECUTABLE_DIMENSION
Definition plastic.cpp:13