33
44//! L2 norm expression for tensor-like types.
55
6- use num_traits:: Float ;
76use prost:: Message ;
87use vortex_array:: ArrayRef ;
9- use vortex_array:: ExecutionCtx ;
10- use vortex_array:: IntoArray ;
11- use vortex_array:: arrays:: Constant ;
12- use vortex_array:: arrays:: ConstantArray ;
13- use vortex_array:: arrays:: ExtensionArray ;
14- use vortex_array:: arrays:: PrimitiveArray ;
158use vortex_array:: arrays:: ScalarFn as ScalarFnArrayEncoding ;
169use vortex_array:: arrays:: ScalarFnArray ;
17- use vortex_array:: arrays:: extension:: ExtensionArrayExt ;
1810use vortex_array:: arrays:: scalar_fn:: ScalarFnArrayExt ;
1911use vortex_array:: arrays:: scalar_fn:: ScalarFnArrayView ;
2012use vortex_array:: arrays:: scalar_fn:: plugin:: ScalarFnArrayParts ;
2113use vortex_array:: arrays:: scalar_fn:: plugin:: ScalarFnArrayVTable ;
2214use vortex_array:: dtype:: DType ;
23- use vortex_array:: dtype:: NativePType ;
24- use vortex_array:: dtype:: Nullability ;
2515use vortex_array:: dtype:: proto:: dtype as pb;
26- use vortex_array:: expr:: Expression ;
27- use vortex_array:: expr:: union_child_validities;
2816use vortex_array:: match_each_float_ptype;
29- use vortex_array:: scalar:: Scalar ;
30- use vortex_array:: scalar_fn:: Arity ;
31- use vortex_array:: scalar_fn:: ChildName ;
3217use vortex_array:: scalar_fn:: EmptyOptions ;
33- use vortex_array:: scalar_fn:: ExecutionArgs ;
3418use vortex_array:: scalar_fn:: ScalarFnId ;
35- use vortex_array:: scalar_fn:: ScalarFnVTable ;
3619use vortex_array:: scalar_fn:: ScalarFnVTableExt ;
20+ use vortex_array:: scalar_fn:: unstable:: row:: InitializedElement ;
21+ use vortex_array:: scalar_fn:: unstable:: row:: RowFn ;
22+ use vortex_array:: scalar_fn:: unstable:: row:: RowVisitor ;
23+ use vortex_array:: scalar_fn:: unstable:: row:: UninitElementSink ;
3724use vortex_array:: serde:: ArrayChildren ;
38- use vortex_buffer:: Buffer ;
39- use vortex_error:: VortexExpect ;
4025use vortex_error:: VortexResult ;
4126use vortex_error:: vortex_err;
4227use vortex_session:: VortexSession ;
4328use vortex_session:: registry:: CachedId ;
4429
45- use crate :: matcher :: AnyTensor ;
46- use crate :: utils :: extract_flat_elements ;
47- use crate :: utils:: validate_tensor_float_input ;
30+ use crate :: scalar_fns :: row :: TensorRow ;
31+ use crate :: scalar_fns :: row :: tensor_element_ptype ;
32+ use crate :: utils:: l2_norm_row ;
4833
4934/// L2 norm (Euclidean norm) of a tensor or vector column.
5035///
5136/// Computes `||v|| = sqrt(sum(v_i^2))` over the flat backing buffer of each tensor-like type.
5237///
5338/// The input must be a tensor-like extension array with a float element type. The output is a float
5439/// column of the same float type.
55- #[ derive( Clone ) ]
40+ #[ derive( Clone , Debug , Default ) ]
5641pub struct L2Norm ;
5742
5843impl L2Norm {
@@ -67,110 +52,41 @@ impl L2Norm {
6752 }
6853}
6954
70- impl ScalarFnVTable for L2Norm {
55+ impl RowFn for L2Norm {
7156 type Options = EmptyOptions ;
7257
58+ const ARG_NAMES : & ' static [ & ' static str ] = & [ "input" ] ;
59+ const INFALLIBLE : bool = true ;
60+
7361 fn id ( & self ) -> ScalarFnId {
7462 static ID : CachedId = CachedId :: new ( "vortex.tensor.l2_norm" ) ;
7563 * ID
7664 }
7765
78- fn arity ( & self , _options : & Self :: Options ) -> Arity {
79- Arity :: Exact ( 1 )
80- }
81-
82- fn child_name ( & self , _options : & Self :: Options , child_idx : usize ) -> ChildName {
83- match child_idx {
84- 0 => ChildName :: from ( "input" ) ,
85- _ => unreachable ! ( "L2Norm must have exactly one child" ) ,
86- }
87- }
88-
89- fn return_dtype ( & self , _options : & Self :: Options , arg_dtypes : & [ DType ] ) -> VortexResult < DType > {
90- let input_dtype = & arg_dtypes[ 0 ] ;
91- let tensor_match = validate_tensor_float_input ( input_dtype) ?;
92- let ptype = tensor_match. element_ptype ( ) ;
93-
94- let nullability = Nullability :: from ( input_dtype. is_nullable ( ) ) ;
95- Ok ( DType :: Primitive ( ptype, nullability) )
66+ fn serialize ( & self , _options : & Self :: Options ) -> VortexResult < Option < Vec < u8 > > > {
67+ Ok ( Some ( vec ! [ ] ) )
9668 }
9769
98- fn execute (
70+ fn deserialize (
9971 & self ,
100- _options : & Self :: Options ,
101- args : & dyn ExecutionArgs ,
102- ctx : & mut ExecutionCtx ,
103- ) -> VortexResult < ArrayRef > {
104- let input_ref = args. get ( 0 ) ?;
105- let row_count = args. row_count ( ) ;
106-
107- let ext = input_ref. dtype ( ) . as_extension ( ) ;
108- let tensor_match = ext
109- . metadata_opt :: < AnyTensor > ( )
110- . vortex_expect ( "we already validated this in `return_dtype`" ) ;
111- let tensor_flat_size = tensor_match. list_size ( ) as usize ;
112- let element_ptype = tensor_match. element_ptype ( ) ;
113-
114- let norm_dtype = DType :: Primitive ( element_ptype, ext. nullability ( ) ) ;
115-
116- // Optimize for the constant array case.
117- if let Some ( array) = input_ref. as_opt :: < Constant > ( ) {
118- let scalar = array. scalar ( ) . as_extension ( ) . to_storage_scalar ( ) ;
119-
120- let Some ( elements) = scalar. as_list ( ) . elements ( ) else {
121- return Ok ( ConstantArray :: new ( Scalar :: null ( norm_dtype) , row_count) . into_array ( ) ) ;
122- } ;
123-
124- let norm_scalar = match_each_float_ptype ! ( element_ptype, |T | {
125- let values: Vec <T > = elements
126- . iter( )
127- . map( |s| {
128- s. as_primitive( )
129- . as_:: <T >( )
130- . vortex_expect( "element was somehow not the correct float" )
131- } )
132- . collect( ) ;
133- let norm = l2_norm_row:: <T >( & values) ;
134-
135- Scalar :: try_new( norm_dtype, Some ( norm. into( ) ) )
136- } ) ?;
137-
138- let norms = ConstantArray :: new ( norm_scalar, row_count) . into_array ( ) ;
139- return Ok ( norms) ;
140- }
141-
142- let input: ExtensionArray = input_ref. execute ( ctx) ?;
143- let validity = input. as_ref ( ) . validity ( ) ?;
144-
145- let storage = input. storage_array ( ) ;
146- let flat = extract_flat_elements ( storage, tensor_flat_size, ctx) ?;
147-
148- match_each_float_ptype ! ( flat. ptype( ) , |T | {
149- let buffer: Buffer <T > = ( 0 ..row_count)
150- . map( |i| l2_norm_row( flat. row:: <T >( i) ) )
151- . collect( ) ;
152-
153- // SAFETY: The buffer length equals `row_count`, which matches the source validity
154- // length.
155- Ok ( unsafe { PrimitiveArray :: new_unchecked( buffer, validity) } . into_array( ) )
156- } )
72+ _metadata : & [ u8 ] ,
73+ _session : & VortexSession ,
74+ ) -> VortexResult < Self :: Options > {
75+ Ok ( EmptyOptions )
15776 }
15877
159- fn validity (
78+ fn dispatch < V : RowVisitor > (
16079 & self ,
16180 _options : & Self :: Options ,
162- expression : & Expression ,
163- ) -> VortexResult < Option < Expression > > {
164- // The result is null if the input tensor is null.
165- union_child_validities ( expression)
166- }
167-
168- fn is_strict ( & self , _options : & Self :: Options ) -> bool {
169- true
170- }
171-
172- fn is_infallible ( & self , _options : & Self :: Options ) -> bool {
173- true
81+ args : & [ DType ] ,
82+ visitor : V ,
83+ ) -> VortexResult < V :: VisitResult > {
84+ match_each_float_ptype ! ( tensor_element_ptype( args) ?, |T | {
85+ visitor. visit_into:: <( TensorRow <T >, ) , UninitElementSink <T >, _>( ( ) , |( row, ) , output| {
86+ // SAFETY: `output` is the `UninitElementSink` row supplied for this callback.
87+ unsafe { InitializedElement :: write( output, l2_norm_row( row) ) }
88+ } )
89+ } )
17490 }
17591}
17692
@@ -217,17 +133,6 @@ impl ScalarFnArrayVTable for L2Norm {
217133 }
218134}
219135
220- /// Computes the L2 norm (Euclidean norm) of a float slice.
221- ///
222- /// Returns `sqrt(sum(v_i^2))`. A zero-length or all-zero input produces `0.0`.
223- fn l2_norm_row < T : Float + NativePType > ( v : & [ T ] ) -> T {
224- let mut sum_sq = T :: zero ( ) ;
225- for & x in v {
226- sum_sq = sum_sq + x * x;
227- }
228- sum_sq. sqrt ( )
229- }
230-
231136#[ cfg( test) ]
232137mod tests {
233138
@@ -258,6 +163,7 @@ mod tests {
258163 use crate :: utils:: test_helpers:: literal_vector_array;
259164 use crate :: utils:: test_helpers:: tensor_array;
260165 use crate :: utils:: test_helpers:: vector_array;
166+ use crate :: utils:: test_helpers:: zero_width_vector_array;
261167
262168 /// Evaluates L2 norm on a tensor/vector array and returns the result as `Vec<f64>`.
263169 fn eval_l2_norm ( input : ArrayRef ) -> VortexResult < Vec < f64 > > {
@@ -267,6 +173,27 @@ mod tests {
267173 Ok ( prim. as_slice :: < f64 > ( ) . to_vec ( ) )
268174 }
269175
176+ fn eval_l2_norm_f32 ( input : ArrayRef ) -> VortexResult < Vec < f32 > > {
177+ let result = L2Norm :: try_new ( input) ?;
178+ let mut ctx = SESSION . create_execution_ctx ( ) ;
179+ let prim: PrimitiveArray = result. into_array ( ) . execute ( & mut ctx) ?;
180+ Ok ( prim. as_slice :: < f32 > ( ) . to_vec ( ) )
181+ }
182+
183+ #[ test]
184+ fn zero_width_and_empty_inputs ( ) -> VortexResult < ( ) > {
185+ assert_close (
186+ & eval_l2_norm ( zero_width_vector_array :: < f64 > ( 3 ) ?) ?,
187+ & [ 0.0 , 0.0 , 0.0 ] ,
188+ ) ;
189+ assert ! ( eval_l2_norm( vector_array( 2 , & [ ] as & [ f64 ] ) ?) ?. is_empty( ) ) ;
190+ assert_close (
191+ & eval_l2_norm ( Vector :: constant_array :: < f64 > ( & [ ] , 3 ) ?) ?,
192+ & [ 0.0 , 0.0 , 0.0 ] ,
193+ ) ;
194+ Ok ( ( ) )
195+ }
196+
270197 #[ rstest]
271198 #[ case:: three_four_five( & [ 2 ] , & [ 3.0 , 4.0 ] , & [ 5.0 ] ) ]
272199 #[ case:: zero_vector( & [ 3 ] , & [ 0.0 , 0.0 , 0.0 ] , & [ 0.0 ] ) ]
@@ -351,6 +278,48 @@ mod tests {
351278 Ok ( ( ) )
352279 }
353280
281+ #[ test]
282+ fn extension_backed_constant_yields_constant_output ( ) -> VortexResult < ( ) > {
283+ let input = Vector :: constant_array ( & [ 3.0f64 , 4.0 ] , 4 ) ?;
284+
285+ let result = L2Norm :: try_new ( input) ?. into_array ( ) ;
286+ let mut ctx = SESSION . create_execution_ctx ( ) ;
287+ let output = result. execute_until :: < Constant > ( & mut ctx) ?;
288+
289+ let constant = output
290+ . as_opt :: < Constant > ( )
291+ . expect ( "L2Norm over constant-backed extension storage must be constant" ) ;
292+ assert_eq ! ( constant. len( ) , 4 ) ;
293+ let norm = constant
294+ . scalar ( )
295+ . as_primitive ( )
296+ . as_ :: < f64 > ( )
297+ . expect ( "norm scalar must be a non-null primitive" ) ;
298+ assert_eq ! ( norm. to_bits( ) , 5.0f64 . to_bits( ) ) ;
299+ Ok ( ( ) )
300+ }
301+
302+ #[ test]
303+ fn encoded_constants_match_materialized_rows_bitwise ( ) -> VortexResult < ( ) > {
304+ let row = [ f32:: MAX , 1.0 , -1.0 ] ;
305+ let materialized = vector_array ( 3 , & [ row, row, row] . concat ( ) ) ?;
306+ let storage_constant = Vector :: constant_array ( & row, 3 ) ?;
307+ let literal_constant = literal_vector_array ( & row, 3 ) ;
308+
309+ let expected: Vec < _ > = eval_l2_norm_f32 ( materialized) ?
310+ . into_iter ( )
311+ . map ( f32:: to_bits)
312+ . collect ( ) ;
313+ for encoded in [ storage_constant, literal_constant] {
314+ let actual: Vec < _ > = eval_l2_norm_f32 ( encoded) ?
315+ . into_iter ( )
316+ . map ( f32:: to_bits)
317+ . collect ( ) ;
318+ assert_eq ! ( actual, expected) ;
319+ }
320+ Ok ( ( ) )
321+ }
322+
354323 /// A constant input whose scalar is null should short-circuit to a null [`ConstantArray`] of
355324 /// the correct primitive dtype and length.
356325 #[ test]
0 commit comments