11import numpy as np
2- from typing import Iterable , Optional
2+ from typing import Iterable , Optional , Callable
33
44from MSUtils .general .MicrostructureImage import MicrostructureImage
55from MSUtils .general .h52xdmf import write_xdmf
6+ from MSUtils .TPMS .tpms_functions import (
7+ gyroid ,
8+ schwarz_p ,
9+ diamond ,
10+ neovius ,
11+ iwp ,
12+ lidinoid ,
13+ )
614
715
816def _to_angle (x , L ):
@@ -15,8 +23,8 @@ class TPMS:
1523
1624 Parameters
1725 ----------
18- tpms_type : str
19- Type of TPMS surface (e.g., ' gyroid', ' schwarz_p', 'diamond', 'neovius', 'iwp', 'lidinoid' ).
26+ func : Callable
27+ Function handle for the TPMS implicit function (e.g., gyroid, schwarz_p).
2028 resolution : tuple of int
2129 Number of voxels in each direction (Nx, Ny, Nz).
2230 L : tuple of float
@@ -35,7 +43,7 @@ class TPMS:
3543
3644 def __init__ (
3745 self ,
38- tpms_type ,
46+ func : Callable [[ np . ndarray , np . ndarray , np . ndarray ], np . ndarray ] ,
3947 resolution : Optional [Iterable [int ]] = (128 , 128 , 128 ),
4048 L : Optional [Iterable [float ]] = (1.0 , 1.0 , 1.0 ),
4149 threshold : Optional [float ] = 0.5 ,
@@ -44,7 +52,7 @@ def __init__(
4452 mode : str = "solid" ,
4553 shell_thickness : float = 0.1 ,
4654 ):
47- self .kind = tpms_type . lower ()
55+ self .func = func
4856 self .resolution = tuple (int (v ) for v in resolution )
4957 self .L = tuple (float (v ) for v in L )
5058 if isinstance (unitcell_frequency , int ):
@@ -62,61 +70,6 @@ def __init__(
6270 self ._field = None # cache for field
6371 self .image = self .generate ()
6472
65- def implicit_function (
66- self , x : np .ndarray , y : np .ndarray , z : np .ndarray
67- ) -> np .ndarray :
68- # Map by frequency: scale coordinates before mapping to angle
69- kx , ky , kz = self .frequency
70- X = _to_angle (x * kx , self .L [0 ])
71- Y = _to_angle (y * ky , self .L [1 ])
72- Z = _to_angle (z * kz , self .L [2 ])
73-
74- kind = self .kind
75- # Standard references: https://minimalsurfaces.blog/home/repository/triply-periodic/
76- # https://kenbrakke.com/evolver/examples/periodic/periodic.html
77- if kind in ("gyroid" ,):
78- # Gyroid: sin(x)cos(y) + sin(y)cos(z) + sin(z)cos(x)
79- return np .sin (X ) * np .cos (Y ) + np .sin (Y ) * np .cos (Z ) + np .sin (Z ) * np .cos (X )
80- if kind in ("schwarz_p" , "p" ):
81- # Schwarz Primitive: cos(x) + cos(y) + cos(z)
82- return np .cos (X ) + np .cos (Y ) + np .cos (Z )
83- if kind in ("schwarz_d" , "diamond" , "d" ):
84- # Diamond: sin(x)sin(y)sin(z) + sin(x)cos(y)cos(z) + cos(x)sin(y)cos(z) + cos(x)cos(y)sin(z)
85- return (
86- np .sin (X ) * np .sin (Y ) * np .sin (Z )
87- + np .sin (X ) * np .cos (Y ) * np .cos (Z )
88- + np .cos (X ) * np .sin (Y ) * np .cos (Z )
89- + np .cos (X ) * np .cos (Y ) * np .sin (Z )
90- )
91- if kind in ("neovius" ,):
92- # Neovius: 3 * (cos(x) + cos(y) + cos(z)) + 4 * cos(x)*cos(y)*cos(z)
93- return 3 * (np .cos (X ) + np .cos (Y ) + np .cos (Z )) + 4 * np .cos (X ) * np .cos (
94- Y
95- ) * np .cos (Z )
96- if kind in ("iwp" ):
97- # I-WP : 2 * (cos(x)cos(y) + cos(y)cos(z) + cos(z)cos(x)) - (cos(2x) + cos(2y) + cos(2z))
98- return 2 * (
99- np .cos (X ) * np .cos (Y ) + np .cos (Y ) * np .cos (Z ) + np .cos (Z ) * np .cos (X )
100- ) - (np .cos (2 * X ) + np .cos (2 * Y ) + np .cos (2 * Z ))
101- if kind in ("lidinoid" ,):
102- # Lidinoid: 0.5 * (sin(2x)cos(y)sin(z) + sin(2y)cos(z)sin(x) + sin(2z)cos(x)sin(y)) - 0.5 * (cos(2x)cos(2y) + cos(2y)cos(2z) + cos(2z)cos(2x)) + 0.15
103- return (
104- 0.5
105- * (
106- np .sin (2 * X ) * np .cos (Y ) * np .sin (Z )
107- + np .sin (2 * Y ) * np .cos (Z ) * np .sin (X )
108- + np .sin (2 * Z ) * np .cos (X ) * np .sin (Y )
109- )
110- - 0.5
111- * (
112- np .cos (2 * X ) * np .cos (2 * Y )
113- + np .cos (2 * Y ) * np .cos (2 * Z )
114- + np .cos (2 * Z ) * np .cos (2 * X )
115- )
116- + 0.15
117- )
118- raise ValueError (f"Unknown or unsupported TPMS kind: { self .kind } " )
119-
12073 def _compute_field (self ):
12174 # Compute and cache the field
12275 Nx , Ny , Nz = self .resolution
@@ -127,7 +80,14 @@ def _compute_field(self):
12780 X = xs [:, None , None ]
12881 Y = ys [None , :, None ]
12982 Z = zs [None , None , :]
130- self ._field = self .implicit_function (X , Y , Z )
83+
84+ # Map by frequency: scale coordinates before mapping to angle
85+ kx , ky , kz = self .frequency
86+ X_ang = _to_angle (X * kx , self .L [0 ])
87+ Y_ang = _to_angle (Y * ky , self .L [1 ])
88+ Z_ang = _to_angle (Z * kz , self .L [2 ])
89+ self ._field = self .func (X_ang , Y_ang , Z_ang )
90+
13191 # range normalize to [0, 1]
13292 self ._field = (self ._field - np .min (self ._field )) / (
13393 np .max (self ._field ) - np .min (self ._field )
@@ -162,8 +122,6 @@ def find_threshold_for_volume_fraction(
162122 target_vf : float ,
163123 tol : float = 1e-3 ,
164124 max_iter : int = 30 ,
165- n_thresh : int = 50 ,
166- optimize : str = "both" ,
167125 ) -> tuple :
168126 """
169127 Find threshold (and shell thickness if mode='shell') for target volume fraction.
@@ -187,97 +145,57 @@ def find_threshold_for_volume_fraction(
187145 flat = field .ravel ()
188146 n_vox = flat .size
189147 if self .mode == "solid" :
190- # For solid: threshold at quantile
148+ # For solid: optimize threshold only
191149 k = int (np .round ((1 - target_vf ) * n_vox ))
192150 sorted_field = np .partition (flat , k )
193151 thr = sorted_field [k ]
194152 self .threshold = thr
195153 return thr , None
196154 elif self .mode == "shell" :
197155 minf , maxf = float (np .min (flat )), float (np .max (flat ))
198- if optimize == "shell_thickness" :
199- # Only optimize shell_thickness, keep threshold fixed
200- thr = self .threshold
201- lo , hi = 0.0 , max (maxf - thr , thr - minf )
202- for _ in range (max_iter ):
203- mid = 0.5 * (lo + hi )
204- vf = np .mean (np .abs (flat - thr ) < mid )
205- err = abs (vf - target_vf )
206- if err < tol :
207- break
208- if vf > target_vf :
209- hi = mid
210- else :
211- lo = mid
212- self .shell_thickness = mid
213- return thr , mid
214- elif optimize == "threshold" :
215- # Only optimize threshold, keep shell_thickness fixed
216- t = abs (self .shell_thickness )
217- best_err = float ("inf" )
218- best_thr = None
219- for thr in np .linspace (minf , maxf , n_thresh ):
220- vf = np .mean (np .abs (flat - thr ) < t )
221- err = abs (vf - target_vf )
222- if err < best_err :
223- best_err = err
224- best_thr = thr
225- if best_err <= tol :
226- break
227- self .threshold = best_thr
228- return best_thr , t
229- elif optimize == "both" :
230- # Jointly optimize threshold and shell_thickness
231- best_err = float ("inf" )
232- best_thr = None
233- best_t = None
234- for thr in np .linspace (minf , maxf , n_thresh ):
235- lo , hi = 0.0 , max (maxf - thr , thr - minf )
236- for _ in range (max_iter ):
237- mid = 0.5 * (lo + hi )
238- vf = np .mean (np .abs (flat - thr ) < mid )
239- err = abs (vf - target_vf )
240- if err < tol :
241- break
242- if vf > target_vf :
243- hi = mid
244- else :
245- lo = mid
246- vf = np .mean (np .abs (flat - thr ) < mid )
247- err = abs (vf - target_vf )
248- if err < best_err :
249- best_err = err
250- best_thr = thr
251- best_t = mid
252- if best_err <= tol :
253- break
254- self .threshold = best_thr
255- self .shell_thickness = best_t
256- return best_thr , best_t
257- else :
258- raise ValueError (f"Unknown optimize mode: { optimize } " )
259- else :
260- raise ValueError (f"Unknown mode: { self .mode } " )
156+ # For shell: optimize shell_thickness only, keep threshold fixed
157+ thr = self .threshold
158+ lo , hi = 0.0 , max (maxf - thr , thr - minf )
159+ for _ in range (max_iter ):
160+ mid = 0.5 * (lo + hi )
161+ vf = np .mean (np .abs (flat - thr ) < mid )
162+ err = abs (vf - target_vf )
163+ if err < tol :
164+ break
165+ if vf > target_vf :
166+ hi = mid
167+ else :
168+ lo = mid
169+ self .shell_thickness = mid
170+ return thr , mid
261171
262172
263173def main ():
264174 N = 512 , 256 , 128
265175 L = 4.0 , 2.0 , 1.0
266- tpms_types = ["gyroid" , "schwarz_p" , "diamond" , "neovius" , "iwp" , "lidinoid" ]
267176 h5_filename = "data/tpms.h5"
268177 unitcell_frequency = (4 , 2 , 1 )
269178 invert = True
270179
271- for tpms_type in tpms_types :
180+ tpms_funcs = {
181+ "gyroid" : gyroid ,
182+ "schwarz_p" : schwarz_p ,
183+ "diamond" : diamond ,
184+ "neovius" : neovius ,
185+ "iwp" : iwp ,
186+ "lidinoid" : lidinoid ,
187+ }
188+
189+ for tpms_type , func in tpms_funcs .items ():
272190 tpms = TPMS (
273- tpms_type = tpms_type ,
191+ func = func ,
274192 resolution = N ,
275193 L = L ,
276194 unitcell_frequency = unitcell_frequency ,
277195 invert = invert ,
278196 mode = "solid" ,
279197 )
280- MS = MicrostructureImage (image = tpms .image )
198+ MS = MicrostructureImage (image = tpms .image , L = L )
281199 MS .write (
282200 h5_filename = h5_filename ,
283201 dset_name = tpms_type ,
0 commit comments