1010
1111
1212def parse_args ():
13- p = argparse .ArgumentParser (description = "CR–P0 steady Navier–Stokes with Nitsche BCs (robust)" )
13+ p = argparse .ArgumentParser (
14+ description = "CR–P0 steady Navier–Stokes with Nitsche BCs (monolithic)"
15+ )
1416 p .add_argument ("--nx" , type = int , default = 64 )
1517 p .add_argument ("--ny" , type = int , default = 32 )
1618 p .add_argument ("--Lx" , type = float , default = 2.0 )
1719 p .add_argument ("--Ly" , type = float , default = 1.0 )
1820 p .add_argument ("--nu" , type = float , default = 1e-2 )
19- p .add_argument ("--uin" , type = float , default = 1.0 )
21+ p .add_argument ("--uin" , type = float , default = 1.0 )
2022 p .add_argument ("--picard-it" , type = int , default = 20 )
2123 p .add_argument ("--picard-tol" , type = float , default = 1e-8 )
2224 p .add_argument ("--theta" , type = float , default = 0.5 )
23- p .add_argument ("--eps-p" , type = float , default = 1e-12 ) # tiny pressure mass to remove nullspace
25+ p .add_argument (
26+ "--eps-p" ,
27+ type = float ,
28+ default = 1e-12 ,
29+ help = "Tiny pressure mass to remove nullspace" ,
30+ )
2431 p .add_argument ("--stokes-only" , action = "store_true" )
2532 p .add_argument ("--no-output" , action = "store_true" )
26- p .add_argument ("--alpha" , type = float , default = 400.0 , help = "Nitsche penalty (100–1000 recommended)" )
27- p .add_argument ("--outfile" , type = str , default = "chapter2/open_cavity_crp0.xdmf" )
33+ p .add_argument (
34+ "--alpha" ,
35+ type = float ,
36+ default = 400.0 ,
37+ help = "Nitsche penalty (100–1000 recommended)" ,
38+ )
39+ # Outfile stem; we append _u.bp / _p.bp
40+ p .add_argument (
41+ "--outfile" ,
42+ type = str ,
43+ default = "chapter2/open_cavity_crp0" ,
44+ help = "Output file stem (without extension)" ,
45+ )
2846 return p .parse_args ()
2947
3048
3149args = parse_args ()
3250comm = MPI .COMM_WORLD
3351rank = comm .rank
3452
35- # --- mesh ---
53+ # --- Mesh ----------------------------------------------------------- ---
3654domain = mesh .create_rectangle (
3755 comm ,
3856 [np .array ([0.0 , 0.0 ]), np .array ([args .Lx , args .Ly ])],
@@ -44,57 +62,79 @@ def parse_args():
4462gdim = domain .geometry .dim
4563cell = domain .basix_cell ()
4664
47- # Ensure needed connectivities
4865domain .topology .create_connectivity (fdim , tdim )
4966domain .topology .create_connectivity (tdim , tdim )
5067
51- # --- boundary facet tags (needed for ds with Nitsche) ---
52- left_facets = mesh .locate_entities_boundary (domain , fdim , lambda x : np .isclose (x [0 ], 0.0 ))
53- right_facets = mesh .locate_entities_boundary (domain , fdim , lambda x : np .isclose (x [0 ], args .Lx ))
54- bottom_facets = mesh .locate_entities_boundary (domain , fdim , lambda x : np .isclose (x [1 ], 0.0 ))
55- top_facets = mesh .locate_entities_boundary (domain , fdim , lambda x : np .isclose (x [1 ], args .Ly ))
68+ # --- Boundary facet tags (for ds & Nitsche) ---------------------------
69+ left_facets = mesh .locate_entities_boundary (
70+ domain , fdim , lambda x : np .isclose (x [0 ], 0.0 )
71+ )
72+ right_facets = mesh .locate_entities_boundary (
73+ domain , fdim , lambda x : np .isclose (x [0 ], args .Lx )
74+ )
75+ bottom_facets = mesh .locate_entities_boundary (
76+ domain , fdim , lambda x : np .isclose (x [1 ], 0.0 )
77+ )
78+ top_facets = mesh .locate_entities_boundary (
79+ domain , fdim , lambda x : np .isclose (x [1 ], args .Ly )
80+ )
5681
5782# Tag: 1=left, 2=right, 3=bottom, 4=top
58- num_facets_local = (
59- domain .topology .index_map (fdim ).size_local
60- + domain .topology .index_map (fdim ).num_ghosts
83+ facet_indices = np .concatenate (
84+ [left_facets , right_facets , bottom_facets , top_facets ]
85+ ).astype (np .int32 )
86+ facet_tags = np .concatenate (
87+ [
88+ np .full_like (left_facets , 1 , dtype = np .int32 ),
89+ np .full_like (right_facets , 2 , dtype = np .int32 ),
90+ np .full_like (bottom_facets , 3 , dtype = np .int32 ),
91+ np .full_like (top_facets , 4 , dtype = np .int32 ),
92+ ]
6193)
62- facet_markers = np .full (num_facets_local , 0 , dtype = np .int32 )
63- facet_markers [left_facets ] = 1
64- facet_markers [right_facets ] = 2
65- facet_markers [bottom_facets ] = 3
66- facet_markers [top_facets ] = 4
67- facet_indices = np .flatnonzero (facet_markers )
68-
69- ft = mesh .meshtags (domain , fdim , facet_indices , facet_markers [facet_indices ])
7094
95+ if facet_indices .size == 0 :
96+ ft = mesh .meshtags (
97+ domain ,
98+ fdim ,
99+ np .array ([], dtype = np .int32 ),
100+ np .array ([], dtype = np .int32 ),
101+ )
102+ else :
103+ order = np .argsort (facet_indices )
104+ ft = mesh .meshtags (domain , fdim , facet_indices [order ], facet_tags [order ])
71105
72106dx = ufl .Measure ("dx" , domain = domain )
73107ds = ufl .Measure ("ds" , domain = domain , subdomain_data = ft )
74108
75- # --- spaces : CR–P0 (mixed) ---
109+ # --- Spaces : CR–P0 (mixed) ----------------------------------------- ---
76110V_el = element ("CR" , cell , 1 , shape = (gdim ,))
77111Q_el = element ("DG" , cell , 0 )
78112W = fem .functionspace (domain , mixed_element ([V_el , Q_el ]))
79113(u , p ) = ufl .TrialFunctions (W )
80114(v , q ) = ufl .TestFunctions (W )
81115
82- # --- parameters / RHS ---
83- nu = fem .Constant (domain , dolfinx . default_scalar_type (args .nu ))
116+ # --- Parameters / RHS ---------------------------------------------- ---
117+ nu = fem .Constant (domain , PETSc . ScalarType (args .nu ))
84118eps_p = fem .Constant (domain , PETSc .ScalarType (args .eps_p ))
85- f_vec = fem .Constant (domain , np .zeros (mesh .geometry .dim , dtype = dolfinx .default_scalar_type ))
119+ zero = fem .Constant (domain , PETSc .ScalarType (0.0 ))
120+ f_vec = ufl .as_vector ((zero , zero )) # zero body force
86121
87- # --- Picard state (for convection) ---
88- w = fem .Function (W ) # holds (u_k, p_k)
122+ # --- Picard state (for convection) -----------------------------------
123+ w = fem .Function (W ) # holds (u_k, p_k)
89124u_k , p_k = w .split ()
90125
126+
91127def build_forms ():
92128 n = ufl .FacetNormal (domain )
93129 h = ufl .CellDiameter (domain )
94130 alpha = PETSc .ScalarType (args .alpha )
95131
96- u_in = ufl .as_vector ((PETSc .ScalarType (args .uin ), PETSc .ScalarType (0.0 )))
97- zero_vec = ufl .as_vector ((PETSc .ScalarType (0.0 ), PETSc .ScalarType (0.0 )))
132+ u_in = ufl .as_vector (
133+ (PETSc .ScalarType (args .uin ), PETSc .ScalarType (0.0 ))
134+ )
135+ zero_vec = ufl .as_vector (
136+ (PETSc .ScalarType (0.0 ), PETSc .ScalarType (0.0 ))
137+ )
98138
99139 # Core Stokes + tiny pressure mass
100140 F = (
@@ -105,69 +145,60 @@ def build_forms():
105145 - ufl .inner (f_vec , v ) * dx
106146 )
107147
108- # Add convection if not stokes -only
148+ # Convection (Picard) if not Stokes -only
109149 if not args .stokes_only :
110150 F += ufl .inner (ufl .dot (u_k , ufl .nabla_grad (u )), v ) * dx
111151
112- # -------- Nitsche + boundary consistency terms on Dirichlet parts --------
113- # Left inlet (tag=1): u = u_in
152+ # --- Nitsche / consistency terms on Dirichlet boundaries - --------
153+ # Tag 1: left inlet, u = u_in
114154 F += (
115- - nu * ufl .inner (ufl .grad (u ) * n , v ) # consistency
116- - nu * ufl .inner (ufl .grad (v ) * n , (u - u_in )) # symmetry
117- + alpha * nu / h * ufl .inner (u - u_in , v ) # penalty
118- - ufl .inner (p * n , v ) # momentum BC consistency
119- + q * ufl .inner (u - u_in , n ) # continuity BC consistency: q * ((u-u_in)· n)
155+ - nu * ufl .inner (ufl .grad (u ) * n , v )
156+ - nu * ufl .inner (ufl .grad (v ) * n , (u - u_in ))
157+ + alpha * nu / h * ufl .inner (u - u_in , v )
158+ - ufl .inner (p * n , v )
159+ + q * ufl .dot (u - u_in , n )
120160 ) * ds (1 )
121161
122- # Bottom (3) and Top (4): no-slip u = 0
123- F += (
124- - nu * ufl .inner (ufl .grad (u ) * n , v )
125- - nu * ufl .inner (ufl .grad (v ) * n , (u - zero_vec ))
126- + alpha * nu / h * ufl .inner (u - zero_vec , v )
127- - ufl .inner (p * n , v )
128- + q * ufl .inner (u - zero_vec , n )
129- ) * ds ((3 ,4 ))
162+ # Tags 3 and 4: bottom + top, u = 0
163+ for tag in (3 , 4 ):
164+ F += (
165+ - nu * ufl .inner (ufl .grad (u ) * n , v )
166+ - nu * ufl .inner (ufl .grad (v ) * n , (u - zero_vec ))
167+ + alpha * nu / h * ufl .inner (u - zero_vec , v )
168+ - ufl .inner (p * n , v )
169+ + q * ufl .dot (u - zero_vec , n )
170+ ) * ds (tag )
130171
131- # Right (2) left as natural outlet (do-nothing)
132- return ufl .system (F )
172+ # Tag 2 (right) kept as natural outlet (do-nothing)
173+ a = ufl .lhs (F )
174+ L = ufl .rhs (F )
175+ return a , L
133176
134177
135178def solve_once ():
136179 a , L = build_forms ()
137180 # No strong BCs with CR → Nitsche handles boundaries
138- A = petsc .assemble_matrix (fem .form (a ), bcs = [])
139- A .assemble ()
140- b = A .createVecRight (); b .set (0.0 )
141- petsc .assemble_vector (b , fem .form (L ))
142- # (No lifting/BC since bcs=[])
143- b .ghostUpdate (addv = PETSc .InsertMode .ADD , mode = PETSc .ScatterMode .REVERSE )
144-
145- if rank == 0 :
146- PETSc .Sys .Print (f" ||b||_2 = { b .norm ():.3e} " )
147-
148- # Robust Krylov (no factorization)
149- ksp = PETSc .KSP ().create (comm )
150- ksp .setOperators (A )
151- ksp .setType ("gmres" )
152- ksp .setTolerances (rtol = 1e-8 , max_it = 1000 )
153- ksp .getPC ().setType ("jacobi" )
154- ksp .setErrorIfNotConverged (True )
155-
156- # wrap dolfinx vector, solve, copy back
157- x = PETSc .Vec ().createWithArray (w .x .array , comm = comm )
158- ksp .solve (b , x )
159- w .x .array [:] = x .getArray ()
160- w .x .scatter_forward ()
161- if rank == 0 :
162- PETSc .Sys .Print (f" ||x||_2 = { x .norm ():.3e} " )
181+ problem = petsc .LinearProblem (
182+ a ,
183+ L ,
184+ u = w ,
185+ bcs = [],
186+ petsc_options = {
187+ "ksp_type" : "gmres" ,
188+ "pc_type" : "jacobi" ,
189+ "ksp_rtol" : 1.0e-8 ,
190+ "ksp_max_it" : 1000 ,
191+ },
192+ petsc_options_prefix = "ns_" ,
193+ )
194+ problem .solve ()
163195
164196
165- # --- Picard loop ---
197+ # --- Picard loop ------------------------------------------------------
166198theta = float (args .theta )
167199tol = float (args .picard_tol )
168200max_it = int (args .picard_it )
169201
170- # for residual check (velocity only)
171202V_sub = W .sub (0 )
172203V0 , _ = V_sub .collapse ()
173204u_prev_fun = fem .Function (V0 )
@@ -176,40 +207,66 @@ def solve_once():
176207for it in range (1 , max_it + 1 ):
177208 solve_once ()
178209
179- # velocity from mixed solution
210+ # Velocity from mixed solution
180211 u_view = w .sub (0 ).collapse ()
181212 u_curr = u_view [0 ] if isinstance (u_view , tuple ) else u_view
182213 u_curr_arr = u_curr .x .array .copy ()
183214
184215 diff = u_curr_arr - u_prev_arr
185- err = float (np .linalg .norm (diff )) if np .all (np .isfinite (diff )) else float ("inf" )
216+ err = float (np .linalg .norm (diff )) if np .all (
217+ np .isfinite (diff )
218+ ) else float ("inf" )
186219
187220 if rank == 0 :
188221 PETSc .Sys .Print (f"Picard { it :02d} : ||u - u_prev|| = { err :.3e} " )
189- PETSc .Sys .Print (f" |u| range: [{ np .abs (u_curr_arr ).min ():.3e} , { np .abs (u_curr_arr ).max ():.3e} ]" )
222+ PETSc .Sys .Print (
223+ f" |u| range: [{ np .abs (u_curr_arr ).min ():.3e} , "
224+ f"{ np .abs (u_curr_arr ).max ():.3e} ]"
225+ )
190226
191227 if not np .isfinite (err ) or err < tol or args .stokes_only :
192228 break
193229
194- # under -relax linearization state u_k := θ u_curr + (1-θ) u_prev
230+ # Under -relax: u_k := θ u_curr + (1-θ) u_prev
195231 relaxed = theta * u_curr_arr + (1.0 - theta ) * u_prev_arr
196- u_k .x .array [:len (relaxed )] = relaxed
232+ u_k .x .array [: len (relaxed )] = relaxed
197233 u_prev_arr = u_curr_arr .copy ()
198234
199- # --- output ---
235+ # --- Output (BP4 / VTKWriter) ------------------------------------- ---
200236if not args .no_output :
237+ # Extract velocity and pressure from mixed solution
201238 u_view = w .sub (0 ).collapse ()
202239 p_view = w .sub (1 ).collapse ()
203240 u_fun = u_view [0 ] if isinstance (u_view , tuple ) else u_view
204241 p_fun = p_view [0 ] if isinstance (p_view , tuple ) else p_view
205242
206- # interpolate to CG1 for cleaner XDMF viewing
207- Vout = fem .functionspace (domain , element ("DG" , cell , 1 , shape = (gdim ,)))
208- Qout = fem .functionspace (domain , element ("Lagrange" , cell , 1 ))
209- u_out = fem .Function (Vout , name = "u" )
243+ # Interpolate to CG1 for smoother visualization
244+ Vout_u = fem .functionspace (
245+ domain , element ("Lagrange" , cell , 1 , shape = (gdim ,))
246+ )
247+ Vout_p = fem .functionspace (domain , element ("Lagrange" , cell , 1 ))
248+
249+ u_out = fem .Function (Vout_u , name = "u" )
210250 u_out .interpolate (u_fun )
211251
212- with io .VTXWriter (domain .comm , args .outfile , [u_out , p_fun ]) as writer :
213- writer .write (0.0 )
252+ p_out = fem .Function (Vout_p , name = "p" )
253+ p_out .interpolate (p_fun )
254+
255+ outfile = args .outfile
256+
257+ # Write velocity
258+ with io .VTXWriter (
259+ domain .comm , outfile + "_u.bp" , u_out , engine = "BP4"
260+ ) as vtx_u :
261+ vtx_u .write (0.0 )
262+
263+ # Write pressure
264+ with io .VTXWriter (
265+ domain .comm , outfile + "_p.bp" , p_out , engine = "BP4"
266+ ) as vtx_p :
267+ vtx_p .write (0.0 )
268+
214269 if rank == 0 :
215- PETSc .Sys .Print (f"Wrote { args .outfile } " )
270+ PETSc .Sys .Print (
271+ f"Wrote { outfile } _u.bp and { outfile } _p.bp"
272+ )
0 commit comments