pytopo3d 0.1.0__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.
pytopo3d/__init__.py ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ """
2
+ Command-line interface utilities for the 3D topology optimization package.
3
+ """
@@ -0,0 +1,207 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Command-line entry point for the pytopo3d package.
4
+ This module provides the main entry point when installed as a package.
5
+ """
6
+
7
+ import sys
8
+ from typing import List, Optional
9
+
10
+ # Import the main function from the main module
11
+ from pytopo3d.cli.parser import parse_args
12
+ from pytopo3d.preprocessing.geometry import load_geometry_data
13
+ from pytopo3d.runners.experiment import (
14
+ execute_optimization,
15
+ export_result_to_stl,
16
+ setup_experiment,
17
+ )
18
+ from pytopo3d.utils.metrics import collect_metrics
19
+ from pytopo3d.visualization.visualizer import (
20
+ create_optimization_animation,
21
+ visualize_final_result,
22
+ visualize_initial_setup,
23
+ )
24
+
25
+
26
+ def main(args: Optional[List[str]] = None) -> int:
27
+ """
28
+ Main function to run the optimization from command-line arguments.
29
+
30
+ Args:
31
+ args: Command line arguments (sys.argv[1:] by default)
32
+
33
+ Returns:
34
+ Exit code (0 for success, non-zero for errors)
35
+ """
36
+ # Parse command-line arguments
37
+ if args is None:
38
+ args = sys.argv[1:]
39
+
40
+ parsed_args = parse_args(args)
41
+
42
+ try:
43
+ # Setup experiment, logging and results manager
44
+ logger, results_mgr = setup_experiment(
45
+ verbose=parsed_args.verbose,
46
+ quiet=parsed_args.quiet,
47
+ log_level=parsed_args.log_level,
48
+ log_file=parsed_args.log_file,
49
+ experiment_name=getattr(parsed_args, "experiment_name", None),
50
+ description=parsed_args.description,
51
+ nelx=parsed_args.nelx,
52
+ nely=parsed_args.nely,
53
+ nelz=parsed_args.nelz,
54
+ volfrac=parsed_args.volfrac,
55
+ penal=parsed_args.penal,
56
+ rmin=parsed_args.rmin,
57
+ )
58
+
59
+ # Update args.experiment_name if it was generated in setup_experiment
60
+ if (
61
+ not hasattr(parsed_args, "experiment_name")
62
+ or not parsed_args.experiment_name
63
+ ):
64
+ parsed_args.experiment_name = results_mgr.experiment_name
65
+
66
+ # Load design space and obstacle data
67
+ design_space_mask, obstacle_mask, combined_obstacle_mask = load_geometry_data(
68
+ nelx=parsed_args.nelx,
69
+ nely=parsed_args.nely,
70
+ nelz=parsed_args.nelz,
71
+ design_space_stl=getattr(parsed_args, "design_space_stl", None),
72
+ pitch=getattr(parsed_args, "pitch", 1.0),
73
+ invert_design_space=getattr(parsed_args, "invert_design_space", False),
74
+ obstacle_config=getattr(parsed_args, "obstacle_config", None),
75
+ experiment_name=parsed_args.experiment_name,
76
+ logger=logger,
77
+ results_mgr=results_mgr,
78
+ )
79
+
80
+ # Create and save initial visualization
81
+ loads_array, constraints_array, _ = visualize_initial_setup(
82
+ nelx=parsed_args.nelx,
83
+ nely=parsed_args.nely,
84
+ nelz=parsed_args.nelz,
85
+ experiment_name=parsed_args.experiment_name,
86
+ logger=logger,
87
+ results_mgr=results_mgr,
88
+ combined_obstacle_mask=combined_obstacle_mask,
89
+ )
90
+
91
+ # Run the optimization
92
+ xPhys, history, run_time = execute_optimization(
93
+ nelx=parsed_args.nelx,
94
+ nely=parsed_args.nely,
95
+ nelz=parsed_args.nelz,
96
+ volfrac=parsed_args.volfrac,
97
+ penal=parsed_args.penal,
98
+ rmin=parsed_args.rmin,
99
+ disp_thres=parsed_args.disp_thres,
100
+ tolx=getattr(parsed_args, "tolx", 0.01),
101
+ maxloop=getattr(parsed_args, "maxloop", 2000),
102
+ create_animation=getattr(parsed_args, "create_animation", False),
103
+ animation_frequency=getattr(parsed_args, "animation_frequency", 10),
104
+ logger=logger,
105
+ combined_obstacle_mask=combined_obstacle_mask,
106
+ )
107
+
108
+ # Save the result to the experiment directory
109
+ result_path = results_mgr.save_result(xPhys, "optimized_design.npy")
110
+ logger.debug(f"Optimization result saved to {result_path}")
111
+
112
+ # Create visualization of the final result
113
+ visualize_final_result(
114
+ nelx=parsed_args.nelx,
115
+ nely=parsed_args.nely,
116
+ nelz=parsed_args.nelz,
117
+ experiment_name=parsed_args.experiment_name,
118
+ disp_thres=parsed_args.disp_thres,
119
+ logger=logger,
120
+ results_mgr=results_mgr,
121
+ xPhys=xPhys,
122
+ combined_obstacle_mask=combined_obstacle_mask,
123
+ loads_array=loads_array,
124
+ constraints_array=constraints_array,
125
+ )
126
+
127
+ # Create animation if history was captured
128
+ gif_path = None
129
+ if history:
130
+ gif_path = create_optimization_animation(
131
+ nelx=parsed_args.nelx,
132
+ nely=parsed_args.nely,
133
+ nelz=parsed_args.nelz,
134
+ experiment_name=parsed_args.experiment_name,
135
+ disp_thres=parsed_args.disp_thres,
136
+ animation_frames=getattr(parsed_args, "animation_frames", 50),
137
+ animation_fps=getattr(parsed_args, "animation_fps", 5),
138
+ logger=logger,
139
+ results_mgr=results_mgr,
140
+ history=history,
141
+ combined_obstacle_mask=combined_obstacle_mask,
142
+ loads_array=loads_array,
143
+ constraints_array=constraints_array,
144
+ )
145
+
146
+ # Export result as STL if requested
147
+ stl_exported = export_result_to_stl(
148
+ export_stl=getattr(parsed_args, "export_stl", False),
149
+ stl_level=getattr(parsed_args, "stl_level", 0.5),
150
+ smooth_stl=getattr(parsed_args, "smooth_stl", False),
151
+ smooth_iterations=getattr(parsed_args, "smooth_iterations", 3),
152
+ logger=logger,
153
+ results_mgr=results_mgr,
154
+ result_path=result_path,
155
+ )
156
+
157
+ # Collect and save metrics
158
+ metrics = collect_metrics(
159
+ nelx=parsed_args.nelx,
160
+ nely=parsed_args.nely,
161
+ nelz=parsed_args.nelz,
162
+ volfrac=parsed_args.volfrac,
163
+ penal=parsed_args.penal,
164
+ rmin=parsed_args.rmin,
165
+ disp_thres=parsed_args.disp_thres,
166
+ tolx=getattr(parsed_args, "tolx", 0.01),
167
+ maxloop=getattr(parsed_args, "maxloop", 2000),
168
+ design_space_stl=getattr(parsed_args, "design_space_stl", None),
169
+ pitch=getattr(parsed_args, "pitch", 1.0),
170
+ obstacle_config=getattr(parsed_args, "obstacle_config", None),
171
+ animation_fps=getattr(parsed_args, "animation_fps", 5),
172
+ stl_level=getattr(parsed_args, "stl_level", 0.5),
173
+ smooth_stl=getattr(parsed_args, "smooth_stl", False),
174
+ smooth_iterations=getattr(parsed_args, "smooth_iterations", 3),
175
+ xPhys=xPhys,
176
+ design_space_mask=design_space_mask,
177
+ obstacle_mask=obstacle_mask,
178
+ combined_obstacle_mask=combined_obstacle_mask,
179
+ run_time=run_time,
180
+ gif_path=gif_path,
181
+ stl_exported=stl_exported,
182
+ )
183
+ results_mgr.update_metrics(metrics)
184
+ logger.debug("Metrics updated")
185
+
186
+ logger.info(f"Optimization complete in {run_time:.2f} seconds.")
187
+ logger.info(f"Result saved to {result_path}")
188
+ logger.info(f"All experiment files are in {results_mgr.experiment_dir}")
189
+
190
+ except Exception as e:
191
+ if "logger" in locals():
192
+ logger.error(f"Error in main function: {e}")
193
+ import traceback
194
+
195
+ logger.debug(f"Error details: {traceback.format_exc()}")
196
+ else:
197
+ print(f"Error during initialization: {e}", file=sys.stderr)
198
+ import traceback
199
+
200
+ print(f"Error details: {traceback.format_exc()}", file=sys.stderr)
201
+ return 1
202
+
203
+ return 0
204
+
205
+
206
+ if __name__ == "__main__":
207
+ sys.exit(main())
pytopo3d/cli/parser.py ADDED
@@ -0,0 +1,235 @@
1
+ """
2
+ Command-line argument parsing for the 3D topology optimization package.
3
+ """
4
+
5
+ import argparse
6
+ import os
7
+ from datetime import datetime
8
+ from typing import Any, Dict, List, Optional
9
+
10
+
11
+ def parse_args(args: Optional[List[str]] = None) -> argparse.Namespace:
12
+ """
13
+ Parse command-line arguments for the topology optimization.
14
+
15
+ Parameters
16
+ ----------
17
+ args : Optional[List[str]], optional
18
+ Command line arguments, by default None (uses sys.argv[1:])
19
+
20
+ Returns
21
+ -------
22
+ argparse.Namespace
23
+ Parsed command-line arguments.
24
+ """
25
+ parser = argparse.ArgumentParser(
26
+ description="3D Topology Optimization",
27
+ formatter_class=argparse.ArgumentDefaultsHelpFormatter,
28
+ )
29
+
30
+ # Basic parameters
31
+ basic_group = parser.add_argument_group("Basic parameters")
32
+ basic_group.add_argument(
33
+ "--nelx", type=int, default=60, help="Number of elements in x direction"
34
+ )
35
+ basic_group.add_argument(
36
+ "--nely", type=int, default=30, help="Number of elements in y direction"
37
+ )
38
+ basic_group.add_argument(
39
+ "--nelz", type=int, default=20, help="Number of elements in z direction"
40
+ )
41
+ basic_group.add_argument(
42
+ "--volfrac", type=float, default=0.3, help="Volume fraction constraint"
43
+ )
44
+ basic_group.add_argument(
45
+ "--penal", type=float, default=3.0, help="Penalty parameter"
46
+ )
47
+ basic_group.add_argument("--rmin", type=float, default=3.0, help="Filter radius")
48
+ basic_group.add_argument(
49
+ "--disp_thres",
50
+ type=float,
51
+ default=0.5,
52
+ help="Threshold for displaying elements in visualization",
53
+ )
54
+ basic_group.add_argument(
55
+ "--tolx",
56
+ type=float,
57
+ default=0.01,
58
+ help="Convergence tolerance on design change",
59
+ )
60
+ basic_group.add_argument(
61
+ "--maxloop",
62
+ type=int,
63
+ default=2000,
64
+ help="Maximum number of iterations",
65
+ )
66
+
67
+ # Output parameters
68
+ output_group = parser.add_argument_group("Output parameters")
69
+ output_group.add_argument(
70
+ "--output",
71
+ type=str,
72
+ default="optimized_design.npy",
73
+ help="Output filename for the optimized design",
74
+ )
75
+ output_group.add_argument(
76
+ "--export-stl",
77
+ action="store_true",
78
+ help="Export the final optimization result as an STL file",
79
+ )
80
+ output_group.add_argument(
81
+ "--stl-level",
82
+ type=float,
83
+ default=0.5,
84
+ help="Contour level for STL export (default: 0.5)",
85
+ )
86
+ output_group.add_argument(
87
+ "--smooth-stl",
88
+ action="store_true",
89
+ default=True,
90
+ help="Apply smoothing to the exported STL (default: True)",
91
+ )
92
+ output_group.add_argument(
93
+ "--smooth-iterations",
94
+ type=int,
95
+ default=5,
96
+ help="Number of smoothing iterations for STL export (default: 5)",
97
+ )
98
+ output_group.add_argument(
99
+ "--experiment-name",
100
+ type=str,
101
+ default=None,
102
+ help="Custom name for the experiment (optional)",
103
+ )
104
+ output_group.add_argument(
105
+ "--description",
106
+ type=str,
107
+ default=None,
108
+ help="Description of the experiment (optional)",
109
+ )
110
+
111
+ # Animation parameters
112
+ animation_group = parser.add_argument_group("Animation parameters")
113
+ animation_group.add_argument(
114
+ "--create-animation",
115
+ action="store_true",
116
+ help="Create a GIF animation of the optimization process",
117
+ )
118
+ animation_group.add_argument(
119
+ "--animation-frequency",
120
+ type=int,
121
+ default=10,
122
+ help="Store every N iterations for the animation (default: 10)",
123
+ )
124
+ animation_group.add_argument(
125
+ "--animation-frames",
126
+ type=int,
127
+ default=50,
128
+ help="Target number of frames to include in the animation (default: 50)",
129
+ )
130
+ animation_group.add_argument(
131
+ "--animation-fps",
132
+ type=int,
133
+ default=5,
134
+ help="Frames per second in the animation (default: 5)",
135
+ )
136
+
137
+ # Design space parameters
138
+ design_space_group = parser.add_argument_group("Design space parameters")
139
+ design_space_group.add_argument(
140
+ "--design-space-stl",
141
+ type=str,
142
+ help="Path to an STL file defining the design space geometry",
143
+ )
144
+ design_space_group.add_argument(
145
+ "--pitch",
146
+ type=float,
147
+ default=1.0,
148
+ help="Distance between voxel centers when voxelizing STL (smaller values create finer detail)",
149
+ )
150
+ design_space_group.add_argument(
151
+ "--invert-design-space",
152
+ action="store_true",
153
+ help="Invert the design space (treat STL as void space rather than design space)",
154
+ )
155
+
156
+ # Obstacle related arguments
157
+ obstacle_group = parser.add_argument_group("Obstacle parameters")
158
+ obstacle_group.add_argument(
159
+ "--obstacle-config", type=str, help="Path to a JSON file defining obstacles"
160
+ )
161
+
162
+ # Logging parameters
163
+ log_group = parser.add_argument_group("Logging parameters")
164
+ log_group.add_argument(
165
+ "--log-level",
166
+ type=str,
167
+ default="INFO",
168
+ choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
169
+ help="Logging level",
170
+ )
171
+ log_group.add_argument("--log-file", type=str, default=None, help="Log file path")
172
+ log_group.add_argument(
173
+ "--verbose",
174
+ "-v",
175
+ action="store_true",
176
+ help="Enable verbose output (DEBUG level)",
177
+ )
178
+ log_group.add_argument(
179
+ "--quiet", "-q", action="store_true", help="Suppress output (WARNING level)"
180
+ )
181
+
182
+ return parser.parse_args(args)
183
+
184
+
185
+ def generate_experiment_name(args: argparse.Namespace) -> str:
186
+ """
187
+ Generate an experiment name from command-line arguments.
188
+
189
+ Parameters
190
+ ----------
191
+ args : argparse.Namespace
192
+ Command-line arguments.
193
+
194
+ Returns
195
+ -------
196
+ str
197
+ Generated experiment name.
198
+ """
199
+ if args.experiment_name:
200
+ return args.experiment_name
201
+
202
+ dims = f"{args.nelx}x{args.nely}x{args.nelz}"
203
+
204
+ # Include obstacle info in experiment name
205
+ obstacle_type = "no_obstacle"
206
+ if args.obstacle_config:
207
+ obstacle_type = os.path.basename(args.obstacle_config).replace(".json", "")
208
+
209
+ # Include design space STL info in experiment name if provided
210
+ design_space = ""
211
+ if hasattr(args, "design_space_stl") and args.design_space_stl:
212
+ stl_name = os.path.basename(args.design_space_stl).replace(".stl", "")
213
+ pitch_info = f"_p{args.pitch}".replace(".", "p")
214
+ design_space = f"_ds_{stl_name}{pitch_info}"
215
+
216
+ return f"{dims}_{obstacle_type}{design_space}"
217
+
218
+
219
+ def create_config_dict(args: argparse.Namespace) -> Dict[str, Any]:
220
+ """
221
+ Create a configuration dictionary from command-line arguments.
222
+
223
+ Parameters
224
+ ----------
225
+ args : argparse.Namespace
226
+ Command-line arguments.
227
+
228
+ Returns
229
+ -------
230
+ Dict[str, Any]
231
+ Configuration dictionary.
232
+ """
233
+ config = vars(args)
234
+ config["timestamp"] = datetime.now().isoformat()
235
+ return config
File without changes
@@ -0,0 +1,32 @@
1
+ """
2
+ Element compliance calculation for 3D topology optimization.
3
+
4
+ This module contains functions for computing element-wise compliance.
5
+ """
6
+
7
+ import numpy as np
8
+
9
+ def element_compliance(U, edofMat, KE):
10
+ """
11
+ Compute element-wise compliance:
12
+ ce[e] = Ue @ KE @ Ue^T for each element e.
13
+
14
+ Parameters
15
+ ----------
16
+ U : ndarray
17
+ Global displacement vector.
18
+ edofMat : ndarray
19
+ Element DOF mapping matrix.
20
+ KE : ndarray
21
+ Element stiffness matrix.
22
+
23
+ Returns
24
+ -------
25
+ ndarray
26
+ A 1D array of length nele with element compliance values.
27
+ """
28
+ nele = edofMat.shape[0]
29
+ # edofMat is 1-based indexing, so subtract 1 for 0-based U
30
+ Ue = U[edofMat.astype(int) - 1] # shape (nele, 24)
31
+ ce = np.sum((Ue @ KE) * Ue, axis=1)
32
+ return ce