src/convection_diffusion.py

changeset 12
a0d61ac3719e
parent 3
c3a4f4bb87f7
equal deleted inserted replaced
11:c4f923017349 12:a0d61ac3719e
62 def __init__( 62 def __init__(
63 self, 63 self,
64 u0, 64 u0,
65 g, 65 g,
66 # w0, 66 # w0,
67 domain_size=[-2, 2, -2, 2], 67 domain_dims=[[-2, 2], [-2, 2]],
68 nx=32, 68 nx=32,
69 ny=32, 69 ny=32,
70 degree=2, 70 degree=2,
71 t0=0.0, 71 t0=0.0,
72 T=1.0, 72 T=1.0,
87 self.alpha = alpha # coercivity lower bound for k 87 self.alpha = alpha # coercivity lower bound for k
88 self.beta = beta # theoretical parameter 88 self.beta = beta # theoretical parameter
89 self.C_emb = C_emb # embedding constant 89 self.C_emb = C_emb # embedding constant
90 self.C_reg = C_reg 90 self.C_reg = C_reg
91 if Delta_t is None: 91 if Delta_t is None:
92 self.Delta_t = T / num_steps # Matches your N=50 intent 92 self.Delta_t = T / num_steps
93 else: 93 else:
94 self.Delta_t = Delta_t 94 self.Delta_t = Delta_t
95 95
96 self.domain_size = domain_size 96 self.domain_dims = domain_dims
97 self.nx = nx 97 self.nx = nx
98 self.ny = ny 98 self.ny = ny
99 self.degree = degree 99 self.degree = degree
100 self.T = T 100 self.T = T
101 self.xbound = xbound 101 self.xbound = xbound
110 self.override_lipschitz_pair = override_lipschitz_pair 110 self.override_lipschitz_pair = override_lipschitz_pair
111 111
112 domain = mesh.create_rectangle( 112 domain = mesh.create_rectangle(
113 MPI.COMM_SELF, 113 MPI.COMM_SELF,
114 [ 114 [
115 np.array([domain_size[0], domain_size[2]]), 115 np.array([domain_dims[0][0], domain_dims[1][0]]),
116 np.array([domain_size[1], domain_size[3]]), 116 np.array([domain_dims[0][1], domain_dims[1][1]]),
117 ], 117 ],
118 [nx, ny], 118 [nx, ny],
119 mesh.CellType.triangle, 119 mesh.CellType.triangle,
120 ) 120 )
121 # Compute the domain volume for the adjoint Lipschitz factor
121 self.domain = domain 122 self.domain = domain
122 # Compute the domain volume for the adjoint Lipschitz factor 123 # Domain diameter from your domain
123 self.domain_size = domain_size
124 # Domain diameter from your domain_size
125 self.diam = np.sqrt( 124 self.diam = np.sqrt(
126 (domain_size[1] - domain_size[0]) ** 2 125 (domain_dims[0][0] - domain_dims[0][1]) ** 2
127 + (domain_size[3] - domain_size[2]) ** 2 126 + (domain_dims[1][0] - domain_dims[1][1]) ** 2
128 ) 127 )
129 128
130 V = fem.functionspace(domain, ("Lagrange", degree)) 129 V = fem.functionspace(domain, ("Lagrange", degree))
131 self.V = V 130 self.V = V
132 131
138 self.g.name = "g" 137 self.g.name = "g"
139 self.g.interpolate(g) 138 self.g.interpolate(g)
140 self.bcs = [] # Initialize BCS as LIST 139 self.bcs = [] # Initialize BCS as LIST
141 self.adjoint_bcs = [] # Initialize ADJOINT BCS as LIST (w = 0) 140 self.adjoint_bcs = [] # Initialize ADJOINT BCS as LIST (w = 0)
142 141
143 # Identify boundary facets for Dirichlet BC location (e.g. left/right boundaries at x=domain_size[0] or x=domain_size[1]) 142 # Identify boundary facets for Dirichlet BC location (e.g. left/right boundaries
144 fdim = domain.topology.dim - 1 143 fdim = domain.topology.dim - 1
145 # Γ₁ facets: TOP/BOTTOM boundaries (y = ymin, ymax) - STATE Dirichlet u = g 144 # Γ₁ facets: TOP/BOTTOM boundaries (y = ymin, ymax) - STATE Dirichlet u = g
146 gamma1_facets = mesh.locate_entities_boundary( 145 gamma1_facets = mesh.locate_entities_boundary(
147 domain, 146 domain,
148 fdim, 147 fdim,
149 lambda x: ( 148 lambda x: (
150 np.isclose(x[1], domain_size[2]) # x[1] = y = ymin 149 np.isclose(x[1], domain_dims[1][0]) # x[1] = y = ymin
151 | np.isclose(x[1], domain_size[3]) 150 | np.isclose(x[1], domain_dims[1][1])
152 ), # x[1] = y = ymax 151 ), # x[1] = y = ymax
153 ) 152 )
154 # Γ₂ facets: LEFT/RIGHT boundaries (x = xmin, xmax) - ADJOINT Dirichlet w = 0 153 # Γ₂ facets: LEFT/RIGHT boundaries (x = xmin, xmax)
155 gamma2_facets = mesh.locate_entities_boundary( 154 gamma2_facets = mesh.locate_entities_boundary(
156 domain, 155 domain,
157 fdim, 156 fdim,
158 lambda x: ( 157 lambda x: (
159 np.isclose(x[0], domain_size[0]) # x[0] = x = xmin 158 np.isclose(x[0], domain_dims[0][0]) # x[0] = x = xmin
160 | np.isclose(x[0], domain_size[1]) 159 | np.isclose(x[0], domain_dims[0][1])
161 ), # x[0] = x = xmax 160 ), # x[0] = x = xmax
162 ) 161 )
162 # tag Γ2 facets (left/right) once in __init__
163 facet_idx = np.concatenate([gamma1_facets, gamma2_facets])
164 facet_vals = np.concatenate(
165 [
166 np.full(len(gamma1_facets), 1, np.int32),
167 np.full(len(gamma2_facets), 2, np.int32),
168 ]
169 )
170 order = np.argsort(facet_idx)
171 facet_tags = mesh.meshtags(domain, fdim, facet_idx[order], facet_vals[order])
163 # STATE BC: u = g on Γ₁ (top/bottom) 172 # STATE BC: u = g on Γ₁ (top/bottom)
164 dofs_gamma1 = fem.locate_dofs_topological(V, fdim, gamma1_facets) 173 dofs_gamma1 = fem.locate_dofs_topological(V, fdim, gamma1_facets)
165 # Create BC and ADD TO LIST 174 # Create BC and ADD TO LIST
166 bc = fem.dirichletbc(self.g, dofs_gamma1) # V inferred from self.g! 175 bc = fem.dirichletbc(self.g, dofs_gamma1) # V inferred from self.g!
167 self.bcs.append(bc) 176 self.bcs.append(bc)
168 177
169 # ADJOINT BC: w = 0 on Γ₁ (same facets, homogenized) 178 # ADJOINT BC: w = 0 on Γ₁ (same facets, homogenized)
170 zero = fem.Constant(domain, PETSc.ScalarType(0.0)) 179 zero = fem.Constant(domain, PETSc.ScalarType(0.0))
171 dofs_gamma2 = fem.locate_dofs_topological(V, fdim, gamma2_facets) 180 # dofs_gamma2 = fem.locate_dofs_topological(V, fdim, gamma2_facets)
172 bc_adjoint = fem.dirichletbc(zero, dofs_gamma2, V) 181 bc_adjoint = fem.dirichletbc(zero, dofs_gamma1, V)
173 self.adjoint_bcs.append(bc_adjoint) 182 self.adjoint_bcs.append(bc_adjoint)
174 183
175 self.fdim = fdim 184 self.fdim = fdim
176 self.boundary_facets = np.unique(np.concatenate((gamma1_facets, gamma2_facets))) 185 # self.boundary_facets = np.unique(np.concatenate((gamma1_facets, gamma2_facets)))
177 186
178 ux = fem.Function(V) 187 ux = fem.Function(V)
179 wpx = fem.Function(V) 188 wpx = fem.Function(V)
180 self.ux = ux 189 self.ux = ux
181 self.wpx = wpx 190 self.wpx = wpx
209 w_n.name = "w_n" 218 w_n.name = "w_n"
210 self.w_n = w_n 219 self.w_n = w_n
211 j_u = fem.Function(V) # New copy! 220 j_u = fem.Function(V) # New copy!
212 j_u.name = "j_u" 221 j_u.name = "j_u"
213 self.j_u = j_u 222 self.j_u = j_u
223 ds = ufl.Measure("ds", domain=domain, subdomain_data=facet_tags)
224 n = ufl.FacetNormal(domain)
214 a2 = ( 225 a2 = (
215 w * v * ufl.dx 226 w * v * ufl.dx
216 + dt 227 + dt * ufl.dot(self.k * ufl.grad(w), ufl.grad(v)) * ufl.dx
217 * ( 228 - dt * (self.c0 * w.dx(0) + self.c1 * w.dx(1)) * v * ufl.dx
218 ufl.dot(self.k * ufl.grad(w), ufl.grad(v)) 229 + dt * (self.c0 * n[0] + self.c1 * n[1]) * w * v * ds(2)
219 - (self.c0 * w.dx(0) * v + self.c1 * w.dx(1) * v)
220 )
221 * ufl.dx
222 ) 230 )
223 self.bilinear_form_w = fem.form(a2) 231 self.bilinear_form_w = fem.form(a2)
224 # Step n=N-1: L2 uses w_n = w^N=0 → solves w^{N-1}. (∂t)_primal^* 232 # Step n=N-1: L2 uses w_n = w^N=0 → solves w^{N-1}. (∂t)_primal^*
225 # Step n=N-2: L2 uses w_n = w^{N-1} → solves w^{N-2} 233 # Step n=N-2: L2 uses w_n = w^{N-1} → solves w^{N-2}
226 # Replace your L2 with 2 simple forms (same notation) 234 # Replace your L2 with 2 simple forms (same notation)
227 235
228 L2 = w_n * v * ufl.dx + dt * j_u * v * ufl.dx # kown w_n (previous step) 236 # No dt scaling in j_u, as that's already included in it.
237 L2 = w_n * v * ufl.dx + j_u * v * ufl.dx # kown w_n (previous step)
229 self.linear_form_w = fem.form(L2) 238 self.linear_form_w = fem.form(L2)
230 239
231 # Solving forward PDE 240 # Solving forward PDE
232 def apply(self, x): 241 def apply(self, x):
233 μ, (k, c) = x 242 μ, (k, c) = x
288 ) 297 )
289 phi = element.tabulate(0, ref) 298 phi = element.tabulate(0, ref)
290 299
291 b_ps.setValuesLocal( 300 b_ps.setValuesLocal(
292 cell_dofs, 301 cell_dofs,
293 alpha * phi, 302 alpha * phi * dt,
294 addv=PETSc.InsertMode.ADD_VALUES, 303 addv=PETSc.InsertMode.ADD_VALUES,
295 ) 304 )
296 305
297 b_ps.assemblyEnd() 306 b_ps.assemblyEnd()
298 307
349 num_steps = len(u_n_list) 358 num_steps = len(u_n_list)
350 359
351 assert len(j) == num_steps, f"MISMATCH: j({len(j)}) != u_n_list({num_steps})" 360 assert len(j) == num_steps, f"MISMATCH: j({len(j)}) != u_n_list({num_steps})"
352 # Terminal condition: w(T) = 0 361 # Terminal condition: w(T) = 0
353 w_n = self.w_n 362 w_n = self.w_n
354 w_n.x.array[:] = 0.0
355 w_n.x.scatter_forward()
356 363
357 self.c0.value = c[0] 364 self.c0.value = c[0]
358 self.c1.value = c[1] 365 self.c1.value = c[1]
359 self.k.value = k 366 self.k.value = k
360 j_u = self.j_u 367 j_u = self.j_u
373 solver.getPC().setType(PETSc.PC.Type.LU) 380 solver.getPC().setType(PETSc.PC.Type.LU)
374 381
375 t = self.t0 + num_steps * dt # Start at time T 382 t = self.t0 + num_steps * dt # Start at time T
376 383
377 # N+1 adjoint values to match N PDE steps 384 # N+1 adjoint values to match N PDE steps
378 w_n_list = [None] * (num_steps + 1) 385 w_n_list = [None] * num_steps
379 whT = fem.Function(V) 386 whT = fem.Function(V)
380 whT.x.array[:] = 0.0 # w^N = 0 387 whT.x.array[:] = 0.0 # w^N = 0
381 whT.x.scatter_forward() 388 whT.x.scatter_forward()
382 w_n_list[num_steps] = whT # Store at FINAL index 389 w_n.x.array[:] = whT.x.array
390 w_n.x.scatter_forward()
383 391
384 # backward steps**: compute w^{N-1}, ..., w^0 392 # backward steps**: compute w^{N-1}, ..., w^0
385 393
386 for i in range(num_steps - 1, -1, -1): 394 for i in range(num_steps - 1, -1, -1):
387 t -= dt 395 t -= dt
434 is_scalar_k = isinstance(k, (int, float, np.number)) 442 is_scalar_k = isinstance(k, (int, float, np.number))
435 is_scalar_c = all(isinstance(ci, (int, float, np.number)) for ci in c) 443 is_scalar_c = all(isinstance(ci, (int, float, np.number)) for ci in c)
436 444
437 # 1. expr1: dual sensitivity w.r.t. μ = ∫₀^T w_p dt (always Function) 445 # 1. expr1: dual sensitivity w.r.t. μ = ∫₀^T w_p dt (always Function)
438 wp_bar = fem.Function(V) 446 wp_bar = fem.Function(V)
439 wp_bar.x.array[:] = ( 447 wp_bar.x.array[:] = dt * sum(w.x.array for w in w_n_list)
440 0.5
441 * dt
442 * (
443 w_n_list[0].x.array
444 + w_n_list[-1].x.array
445 + 2 * np.sum([w.x.array for w in w_n_list[1:-1]], axis=0)
446 )
447 )
448 448
449 ux = self.ux 449 ux = self.ux
450 wpx = self.wpx 450 wpx = self.wpx
451 451
452 # 2. expr2: dual sensitivity w.r.t. k 452 # 2. expr2: dual sensitivity w.r.t. k
460 ux.x.array[:] = u.x.array[:] 460 ux.x.array[:] = u.x.array[:]
461 ux.x.scatter_forward() 461 ux.x.scatter_forward()
462 wpx.x.array[:] = wp.x.array[:] 462 wpx.x.array[:] = wp.x.array[:]
463 wpx.x.scatter_forward() 463 wpx.x.scatter_forward()
464 v2 = fem.assemble_scalar(form) 464 v2 = fem.assemble_scalar(form)
465 weight = 0.5 * dt if (n == 0 or n == len(u_n_list) - 1) else dt 465 expr2 -= dt * v2
466 expr2 -= weight * v2
467 466
468 else: 467 else:
469 raise Exception("Unimplemented - out of date") 468 raise Exception("Unimplemented - out of date")
470 469
471 # k Function → ⟨expr2, δk⟩ = ∫ expr2 δk → expr2 = ∇u·∇w_p (pointwise) 470 # k Function → ⟨expr2, δk⟩ = ∫ expr2 δk → expr2 = ∇u·∇w_p (pointwise)
473 expr2a = expr2.x.array 472 expr2a = expr2.x.array
474 473
475 for n, (u, wp) in enumerate(zip(u_n_list, w_n_list)): 474 for n, (u, wp) in enumerate(zip(u_n_list, w_n_list)):
476 form = ufl.inner(ufl.grad(u), ufl.grad(wp)) * ufl.dx 475 form = ufl.inner(ufl.grad(u), ufl.grad(wp)) * ufl.dx
477 v2 = fem.assemble_scalar(fem.form(form)) 476 v2 = fem.assemble_scalar(fem.form(form))
478 weight = 0.5 * dt if (n == 0 or n == len(u_n_list) - 1) else dt 477 expr2a -= dt * v2
479 expr2a -= weight * v2
480 478
481 # 3. expr3: dual sensitivity w.r.t. c = (c1, c2) 479 # 3. expr3: dual sensitivity w.r.t. c = (c1, c2)
482 if is_scalar_c: 480 if is_scalar_c:
483 # Scalar c → return tuple of 2 floats 481 # Scalar c → return tuple of 2 floats
484 482
490 ux.x.array[:] = u.x.array[:] 488 ux.x.array[:] = u.x.array[:]
491 ux.x.scatter_forward() 489 ux.x.scatter_forward()
492 wpx.x.array[:] = wp.x.array[:] 490 wpx.x.array[:] = wp.x.array[:]
493 wpx.x.scatter_forward() 491 wpx.x.scatter_forward()
494 v3 = fem.assemble_scalar(forms[i]) 492 v3 = fem.assemble_scalar(forms[i])
495 weight = 0.5 * dt if (n == 0 or n == len(u_n_list) - 1) else dt 493 expr3i -= dt * v3
496 expr3i -= weight * v3
497 return expr3i 494 return expr3i
498 495
499 expr3 = tuple(val(i) for i in range(dim)) 496 expr3 = tuple(val(i) for i in range(dim))
500 497
501 else: 498 else:
507 expr3i = fem.Function(V) 504 expr3i = fem.Function(V)
508 expr3ia = expr3i.x.array[:] 505 expr3ia = expr3i.x.array[:]
509 for n, (u, wp) in enumerate(zip(u_n_list, w_n_list)): 506 for n, (u, wp) in enumerate(zip(u_n_list, w_n_list)):
510 form = wp * ufl.grad(u)[i] * ufl.dx 507 form = wp * ufl.grad(u)[i] * ufl.dx
511 v3 = fem.assemble_scalar(fem.form(form)) 508 v3 = fem.assemble_scalar(fem.form(form))
512 weight = 0.5 * dt if (n == 0 or n == len(u_n_list) - 1) else dt 509 expr3ia -= dt * v3
513 expr3ia -= weight * v3
514 return expr3i 510 return expr3i
515 511
516 expr3 = tuple(val(i) for i in range(dim)) 512 expr3 = tuple(val(i) for i in range(dim))
517 513
518 if self.save_for_plot: 514 if self.save_for_plot:
779 """ 775 """
780 776
781 def _own(self, x): 777 def _own(self, x):
782 return x if isinstance(x, float) else own(x) 778 return x if isinstance(x, float) else own(x)
783 779
784 def __init__(self, α, base=None): 780 def __init__(self, α, base=None, τscale=None):
785 self.α = α 781 self.α = α
782 self.τscale = τscale
786 if base is not None: 783 if base is not None:
787 (k, (c1, c2)) = base 784 (k, (c1, c2)) = base
788 self.base = (self._own(k), (self._own(c1), self._own(c2))) 785 self.base = (self._own(k), (self._own(c1), self._own(c2)))
789 self.base_norm_squared = (nn2(k), (nn2(c1), nn2(c2))) 786 self.base_norm_squared = (nn2(k), (nn2(c1), nn2(c2)))
790 else: 787 else:
844 if isinstance(self.α, float): 841 if isinstance(self.α, float):
845 αk, αc = self.α, self.α 842 αk, αc = self.α, self.α
846 else: 843 else:
847 αk, αc = self.α 844 αk, αc = self.α
848 845
846 if self.τscale is not None:
847 kscale, cscale = self.τscale
848 αk = αk * kscale
849 αc = αc * cscale
850
849 return ( 851 return (
850 self._prox1(τ * αk, k, kb), 852 self._prox1(τ * αk, k, kb),
851 (self._prox1(τ * αc, c1, c1b), self._prox1(τ * αc, c2, c2b)), 853 (self._prox1(τ * αc, c1, c1b), self._prox1(τ * αc, c2, c2b)),
852 ) 854 )
853 855
993 Quadratic regularisation with box contraints 995 Quadratic regularisation with box contraints
994 """ 996 """
995 997
996 998
997 class BoxedQuadraticRegularisation: 999 class BoxedQuadraticRegularisation:
998 def __init__(self, ω0, ω1, α, base=None): 1000 def __init__(self, ω0, ω1, α, base=None, τscale=None):
999 self.box = BoxConstraints(ω0, ω1) 1001 self.box = BoxConstraints(ω0, ω1)
1000 self.quadratic = QuadraticRegularisation(α, base) 1002 self.quadratic = QuadraticRegularisation(α, base, τscale)
1001 1003
1002 def apply(self, x): 1004 def apply(self, x):
1003 return self.box.apply(x) + self.quadratic.apply(x) 1005 return self.box.apply(x) + self.quadratic.apply(x)
1004 1006
1005 def prox(self, τ, x): 1007 def prox(self, τ, x):

mercurial