| 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 |
| 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 |
| 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: |
| 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): |