| 58 # SVD operator norm |
63 # SVD operator norm |
| 59 _u, s, _v = svds(self.R, k=1) |
64 _u, s, _v = svds(self.R, k=1) |
| 60 self._opnorm = s[0] # / sigma |
65 self._opnorm = s[0] # / sigma |
| 61 # print(f"Laser sampling opnorm = {self._opnorm:.6f}") |
66 # print(f"Laser sampling opnorm = {self._opnorm:.6f}") |
| 62 |
67 |
| |
68 # ------------------------------------------------------------------ |
| |
69 # Exact per-cell line integration |
| |
70 # |
| |
71 # A beam is split into the ordered sub-segments where it actually |
| |
72 # crosses each element (via Cyrus-Beck clipping against the cell's |
| |
73 # polygon), and each sub-segment is integrated exactly using the |
| |
74 # 3-point Simpson rule (exact for phi_h of degree <= 2, since phi_h |
| |
75 # pulled back along a straight line stays degree <= 2 in the |
| |
76 # parameter t). This replaces the previous approach of sampling many |
| |
77 # midpoints and splitting weight equally across a cell's dofs. |
| |
78 # ------------------------------------------------------------------ |
| |
79 |
| |
80 def _cell_vertices_2d(self, cell_idx): |
| |
81 """Physical (x, y) vertices of a cell, ordered CCW around its centroid.""" |
| |
82 x_geom = self.domain.geometry.x |
| |
83 verts = self.domain.geometry.dofmap[cell_idx] |
| |
84 pts = x_geom[verts, :2] |
| |
85 centroid = pts.mean(axis=0) |
| |
86 angles = np.arctan2(pts[:, 1] - centroid[1], pts[:, 0] - centroid[0]) |
| |
87 return pts[np.argsort(angles)] |
| |
88 |
| |
89 def _clip_segment_to_cell(self, pts, x, y, eps=1e-12): |
| |
90 """ |
| |
91 Cyrus-Beck clip of the segment x->y against the convex polygon `cell_idx`. |
| |
92 Returns (t0, t1) in [0, 1] such that x + t*(y - x), t in [t0, t1], is the |
| |
93 part of the segment inside the cell -- or None if there's no overlap. |
| |
94 Works for any convex cell (triangle or quad); assumes straight-sided |
| |
95 (affine, P1) geometry. |
| |
96 """ |
| |
97 n_verts = len(pts) |
| |
98 d = y - x |
| |
99 t_lo, t_hi = 0.0, 1.0 |
| |
100 |
| |
101 for i in range(n_verts): |
| |
102 v0 = pts[i] |
| |
103 v1 = pts[(i + 1) % n_verts] |
| |
104 edge = v1 - v0 |
| |
105 normal = np.array([edge[1], -edge[0]]) # outward normal (CCW polygon) |
| |
106 |
| |
107 denom = normal @ d |
| |
108 numer = normal @ (v0 - x) |
| |
109 |
| |
110 if abs(denom) < eps: |
| |
111 if numer < -eps: # segment parallel to edge, and entirely outside it |
| |
112 return None |
| |
113 continue # parallel and inside: this edge imposes no constraint |
| |
114 |
| |
115 t_edge = numer / denom |
| |
116 if denom > 0: |
| |
117 t_hi = min(t_hi, t_edge) |
| |
118 else: |
| |
119 t_lo = max(t_lo, t_edge) |
| |
120 |
| |
121 if t_lo > t_hi + eps: |
| |
122 return None |
| |
123 |
| |
124 if t_lo > t_hi: |
| |
125 return None |
| |
126 return t_lo, t_hi |
| |
127 |
| 63 def matrix(self, beams): |
128 def matrix(self, beams): |
| 64 """Combined: generate beams + segment + cell collision + dof weighting""" |
129 """Combined: exact beam/cell intersection + exact per-cell line integration.""" |
| 65 dofmap = self.V.dofmap |
|
| 66 tdim = self.domain.topology.dim |
|
| 67 |
|
| 68 # Geometry trees |
|
| 69 |
|
| 70 bb_tree = geometry.bb_tree(self.domain, tdim) |
|
| 71 |
|
| 72 n_dofs = self.V.dofmap.index_map.size_global |
130 n_dofs = self.V.dofmap.index_map.size_global |
| 73 R = np.zeros((self.M, n_dofs)) |
131 # R = np.zeros((self.M, n_dofs)) |
| 74 |
132 rows, cols, data = [], [], [] |
| 75 xmin, xmax, ymin, ymax = self.domain_size |
133 |
| 76 |
134 for cell_idx in range(len(self.domain.geometry.dofmap)): |
| 77 for beam_idx, (start_pt, end_pt) in enumerate(beams): |
135 pts = self._cell_vertices_2d(cell_idx) |
| 78 # Segment into midpoints |
136 for beam_idx, (start_pt, end_pt) in enumerate(beams): |
| 79 vec = end_pt - start_pt |
137 clip = self._clip_segment_to_cell(pts, start_pt, end_pt) |
| 80 total_len = np.linalg.norm(vec) |
138 if clip is not None: |
| 81 seg_len = total_len / self.num_segments |
139 t0, t1 = clip |
| 82 |
140 if t1 - t0 > 1e-10: |
| 83 midpoints = [] |
141 beam_v = end_pt - start_pt |
| 84 for i in range(self.num_segments): |
142 x0 = start_pt + t0 * beam_v |
| 85 t = (i + 0.5) / self.num_segments |
143 x1 = start_pt + t1 * beam_v |
| 86 midpoint = start_pt + t * vec |
144 midpt = start_pt + (0.5 * (t1 + t0)) * beam_v |
| 87 midpoints.append(midpoint) |
145 """ |
| 88 # midpoints = np.array(midpoints, dtype=np.float64) # Force float64 |
146 Exact int_{x0}^{x1} phi_h ds for phi_h in V restricted to `cell_idx`, as a |
| 89 |
147 (global_dofs, row_values) pair such that row_values @ phi_h.x.array[global_dofs] |
| 90 # Ensure shape (N, 3), C-contiguous, read-only for DOLFINx |
148 equals the integral. Uses the 3-point Simpson rule (endpoints + midpoint), |
| 91 # assert midpoints.shape[1] == 2, ( |
149 which is exact since phi_h has degree <= 2 along the line. |
| 92 # "Expected 2D points, got shape[1]=2 but need padding to 3D" |
150 """ |
| 93 # ) |
151 x_geom = self.domain.geometry.x |
| 94 # midpoints = np.pad( |
152 verts = self.domain.geometry.dofmap[cell_idx] |
| 95 # midpoints, ((0, 0), (0, 1)), mode="constant" |
153 v0, v1, v2 = ( |
| 96 # ) # (N, 3) with z=0 |
154 x_geom[verts[0], :2], |
| 97 # midpoints = np.ascontiguousarray(midpoints) # C-order |
155 x_geom[verts[1], :2], |
| 98 # midpoints.setflags(write=False) # Read-only requirement |
156 x_geom[verts[2], :2], |
| 99 |
157 ) |
| 100 # Find colliding cells |
158 J = np.column_stack([v1 - v0, v2 - v0]) |
| 101 # cell_candidates = geometry.compute_collisions_points(bb_tree, midpoints) |
159 """Map a physical point p to reference coordinates on an affine triangle.""" |
| 102 # colliding_cells = geometry.compute_colliding_cells( |
160 pts_ref = np.array( |
| 103 # self.domain, cell_candidates, midpoints |
161 [ |
| 104 # ) |
162 np.linalg.solve(J, x0 - v0), |
| 105 |
163 np.linalg.solve(J, midpt - v0), |
| 106 # Cell→DOFs→weight accumulation |
164 np.linalg.solve(J, x1 - v0), |
| 107 # cell_lists = list(colliding_cells.array) |
165 ] |
| 108 # for seg_i, cell_idx_raw in enumerate(cell_lists): |
166 ) |
| 109 # cell_idx = int(cell_idx_raw) # Convert np.int32 → Python int |
167 tab = self._element.tabulate(0, pts_ref)[ |
| 110 for point in midpoints: |
168 0 |
| 111 pad_point = np.array([point[0], point[1], 0.0]) |
169 ] # (npoints, ndof_local, value_size) |
| 112 cell_idx = cell_FunctionSpace_f64(self.V._cpp_object, pad_point) |
170 tab = tab[ |
| 113 if cell_idx >= 0: # Valid cell index |
171 ..., 0 |
| 114 # Get global DOFs |
172 ] # scalar element -> drop trailing value_size=1 axis |
| 115 cell_dofs_local = dofmap.cell_dofs(cell_idx) |
173 |
| 116 # Convert local dofs to numpy int32 array (DOLFINx requirement) |
174 length = (t1 - t0) * np.linalg.norm(beam_v) |
| 117 # local_dofs_array = np.asarray(cell_dofs_local, dtype=np.int32) |
175 weights = np.array([1.0, 4.0, 1.0]) * length / 6.0 |
| 118 # local_dofs_array.setflags(write=False) # Read-only requirement |
176 local_dofs = self.V.dofmap.cell_dofs(cell_idx) |
| 119 |
177 # global_dofs = self.V.dofmap.index_map.local_to_global(local_dofs) |
| 120 # Batch convert all local indices to global in one call |
178 # R[beam_idx, local_dofs] += weights @ tab |
| 121 cell_dofs_global = dofmap.index_map.local_to_global(cell_dofs_local) |
179 rows.append(np.full(len(local_dofs), beam_idx, dtype=np.int64)) |
| 122 cell_dofs = np.unique(cell_dofs_global) |
180 cols.append(np.asarray(local_dofs, dtype=np.int64)) |
| 123 |
181 data.append(weights @ tab) |
| 124 weight = seg_len / len(cell_dofs) |
182 |
| 125 R[beam_idx, cell_dofs] += weight # ADD for overlaps! |
183 # return R |
| 126 |
184 |
| 127 return R |
185 if rows: |
| |
186 rows = np.concatenate(rows) |
| |
187 cols = np.concatenate(cols) |
| |
188 data = np.concatenate(data) |
| |
189 else: |
| |
190 rows = np.array([], dtype=np.int64) |
| |
191 cols = np.array([], dtype=np.int64) |
| |
192 data = np.array([], dtype=np.float64) |
| |
193 |
| |
194 return coo_matrix((data, (rows, cols)), shape=(self.M, n_dofs)).tocsr() |
| 128 |
195 |
| 129 def apply(self, x): |
196 def apply(self, x): |
| 130 """ |
197 """ |
| 131 This does not work with MPI |
198 This does not work with MPI |
| 132 """ |
199 """ |