Skip to content

Commit 42d3052

Browse files
committed
Refactor shape validation into helpers and extend across all BCs/ICs
1 parent ccdfd11 commit 42d3052

3 files changed

Lines changed: 320 additions & 40 deletions

File tree

deepxde/icbc/boundary_conditions.py

Lines changed: 76 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,52 @@
2525
from .. import utils
2626
from ..backend import backend_name
2727

28+
def _check_target_values(values, n_points, n_components, cls_name):
29+
"""Validate fixed target values at construction time.
30+
31+
Scalars are allowed and broadcast intentionally. Arrays must match
32+
``(n_points, n_components)`` exactly, since a ``(N,)`` vs ``(N, 1)``
33+
mismatch would otherwise broadcast to ``(N, N)`` in the loss.
34+
"""
35+
if isinstance(values, numbers.Number):
36+
return
37+
38+
shape = np.asarray(values).shape
39+
if shape != (n_points, n_components):
40+
raise ValueError(
41+
f"{cls_name}: `values` must have shape "
42+
f"{(n_points, n_components)} (n_points, n_components); got {shape}."
43+
)
44+
45+
46+
def _check_func_output(values, cls_name):
47+
"""Validate the output of a user-supplied BC function at run time.
48+
49+
Allows 0-d scalars (e.g. ``lambda x: 0``), which broadcast harmlessly
50+
against an ``(N, 1)`` slice. Rejects 1-D and wide 2-D outputs, which
51+
broadcast silently to ``(N, N)``. Row count is already guaranteed by
52+
the ``[beg:end]`` slicing and is not re-checked.
53+
54+
``values`` may arrive as a backend tensor or as a raw Python/NumPy value.
55+
The helper detects either representation and reports the same contract.
56+
"""
57+
try:
58+
nd = bkd.ndim(values)
59+
shape = bkd.shape(values)
60+
except (AttributeError, TypeError, ValueError):
61+
arr = np.asarray(values)
62+
nd = arr.ndim
63+
shape = arr.shape
64+
65+
if nd == 0:
66+
return
67+
if nd != 2 or shape[1] != 1:
68+
raise RuntimeError(
69+
f"{cls_name}: func should return an array of shape N by 1 for each "
70+
"component. Use the argument 'component' for different output "
71+
"components."
72+
)
73+
2874

2975
class BC(ABC):
3076
"""Boundary condition base class.
@@ -73,11 +119,7 @@ def __init__(self, geom, func, on_boundary, component=0):
73119

74120
def error(self, X, inputs, outputs, beg, end, aux_var=None):
75121
values = self.func(X, beg, end, aux_var)
76-
if bkd.ndim(values) == 2 and bkd.shape(values)[1] != 1:
77-
raise RuntimeError(
78-
"DirichletBC function should return an array of shape N by 1 for each "
79-
"component. Use argument 'component' for different output components."
80-
)
122+
_check_func_output(values, "DirichletBC")
81123
return outputs[beg:end, self.component : self.component + 1] - values
82124

83125

@@ -90,6 +132,7 @@ def __init__(self, geom, func, on_boundary, component=0):
90132

91133
def error(self, X, inputs, outputs, beg, end, aux_var=None):
92134
values = self.func(X, beg, end, aux_var)
135+
_check_func_output(values, "NeumannBC")
93136
return self.normal_derivative(X, inputs, outputs, beg, end) - values
94137

95138

@@ -126,13 +169,12 @@ def collocation_points(self, X):
126169
def error(self, X, inputs, outputs, beg, end, aux_var=None):
127170
mid = beg + (end - beg) // 2
128171
if self.derivative_order == 0:
129-
yleft = outputs[beg:mid, self.component : self.component + 1]
130-
yright = outputs[mid:end, self.component : self.component + 1]
131-
else:
132-
dydx = grad.jacobian(outputs, inputs, i=self.component, j=self.component_x)
133-
yleft = dydx[beg:mid]
134-
yright = dydx[mid:end]
135-
return yleft - yright
172+
return (
173+
outputs[beg:mid, self.component : self.component + 1]
174+
- outputs[mid:end, self.component : self.component + 1]
175+
)
176+
dydx = grad.jacobian(outputs, inputs, i=self.component, j=self.component_x)
177+
return dydx[beg:mid] - dydx[mid:end]
136178

137179

138180
class OperatorBC(BC):
@@ -159,7 +201,10 @@ def __init__(self, geom, func, on_boundary):
159201
self.func = func
160202

161203
def error(self, X, inputs, outputs, beg, end, aux_var=None):
162-
return self.func(inputs, outputs, X)[beg:end]
204+
values = self.func(inputs, outputs, X)[beg:end]
205+
_check_func_output(values, "OperatorBC")
206+
return values
207+
163208

164209

165210
class PointSetBC:
@@ -184,13 +229,18 @@ class PointSetBC:
184229

185230
def __init__(self, points, values, component=0, batch_size=None, shuffle=True):
186231
self.points = np.array(points, dtype=config.real(np))
187-
self.values = bkd.as_tensor(values, dtype=config.real(bkd.lib))
188232
self.component = component
189-
if isinstance(component, list) and backend_name != "pytorch":
190-
# TODO: Add support for multiple components in other backends
191-
raise RuntimeError(
192-
"multiple components only implemented for pytorch backend"
193-
)
233+
if isinstance(component, list):
234+
if backend_name != "pytorch":
235+
# TODO: Add support for multiple components in other backends
236+
raise RuntimeError(
237+
"multiple components only implemented for pytorch backend"
238+
)
239+
n_components = len(component)
240+
else:
241+
n_components = 1
242+
_check_target_values(values, len(self.points), n_components, "PointSetBC")
243+
self.values = bkd.as_tensor(values, dtype=config.real(bkd.lib))
194244
self.batch_size = batch_size
195245

196246
if batch_size is not None: # batch iterator and state
@@ -257,17 +307,7 @@ class PointSetOperatorBC:
257307

258308
def __init__(self, points, values, func, batch_size=None, shuffle=True):
259309
self.points = np.array(points, dtype=config.real(np))
260-
if not isinstance(values, numbers.Number):
261-
values_arr = np.asarray(values)
262-
if values_arr.ndim != 2 or values_arr.shape[1] != 1:
263-
raise RuntimeError(
264-
f"PointSetOperatorBC received values of shape {values_arr.shape}, "
265-
f"expected 2D array of shape (N, 1)."
266-
)
267-
if values_arr.shape[0] != len(self.points):
268-
raise RuntimeError(
269-
f"PointSetOperatorBC received {len(self.points)} points but {values_arr.shape[0]} values."
270-
)
310+
_check_target_values(values, len(self.points), 1, "PointSetOperatorBC")
271311
self.values = bkd.as_tensor(values, dtype=config.real(bkd.lib))
272312
self.func = func
273313
self.batch_size = batch_size
@@ -290,9 +330,12 @@ def collocation_points(self, X):
290330
return self.points
291331

292332
def error(self, X, inputs, outputs, beg, end, aux_var=None):
333+
values = self.func(inputs, outputs, X)[beg:end]
334+
_check_func_output(values, "PointSetOperatorBC")
293335
if self.batch_size is not None:
294-
return self.func(inputs, outputs, X)[beg:end] - self.values[self.batch_indices]
295-
return self.func(inputs, outputs, X)[beg:end] - self.values
336+
return values - self.values[self.batch_indices]
337+
return values - self.values
338+
296339

297340

298341
class Interface2DBC:
@@ -361,8 +404,7 @@ def error(self, X, inputs, outputs, beg, end, aux_var=None):
361404
this is likely because the chosen edges do not have the same length."
362405
)
363406
values = self.func(X, beg, mid, aux_var)
364-
if bkd.ndim(values) == 2 and bkd.shape(values)[1] != 1:
365-
raise RuntimeError("BC function should return an array of shape N by 1")
407+
_check_func_output(values, "Interface2DBC")
366408
left_n = self.boundary_normal(X, beg, mid, None)
367409
right_n = self.boundary_normal(X, mid, end, None)
368410
if self.direction == "normal":

deepxde/icbc/initial_conditions.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import numpy as np
66

7-
from .boundary_conditions import npfunc_range_autocache
7+
from .boundary_conditions import _check_func_output, npfunc_range_autocache
88
from .. import backend as bkd
99
from .. import utils
1010

@@ -28,9 +28,5 @@ def collocation_points(self, X):
2828

2929
def error(self, X, inputs, outputs, beg, end, aux_var=None):
3030
values = self.func(X, beg, end, aux_var)
31-
if bkd.ndim(values) == 2 and bkd.shape(values)[1] != 1:
32-
raise RuntimeError(
33-
"IC function should return an array of shape N by 1 for each component."
34-
"Use argument 'component' for different output components."
35-
)
31+
_check_func_output(values, "IC")
3632
return outputs[beg:end, self.component : self.component + 1] - values

0 commit comments

Comments
 (0)