2525from .. import utils
2626from ..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
2975class 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
138180class 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
165210class 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
298341class 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" :
0 commit comments