Skip to main content

subtr_actor/collector/ndarray/
traits.rs

1use crate::stats::analysis_graph::{AnalysisDependency, AnalysisGraph};
2use crate::*;
3/// Re-export of `derive_new` used by the public ndarray feature macros.
4pub use ::derive_new;
5/// Re-export of `paste` used by the public ndarray feature macros.
6pub use ::paste;
7use boxcars;
8use std::any::type_name;
9use std::marker::PhantomData;
10use std::sync::Arc;
11
12/// Typed, read-only view of analysis state available to analysis-backed features.
13pub struct AnalysisFeatureContext<'a> {
14    graph: &'a AnalysisGraph,
15}
16
17impl<'a> AnalysisFeatureContext<'a> {
18    pub(crate) fn new(graph: &'a AnalysisGraph) -> Self {
19        Self { graph }
20    }
21
22    pub fn maybe_state<T: 'static>(&self) -> Option<&'a T> {
23        self.graph.state::<T>()
24    }
25
26    pub fn state<T: 'static>(&self) -> SubtrActorResult<&'a T> {
27        self.maybe_state::<T>().ok_or_else(|| {
28            SubtrActorError::new(SubtrActorErrorVariant::CallbackError(format!(
29                "missing analysis state {}",
30                type_name::<T>(),
31            )))
32        })
33    }
34}
35
36/// Object-safe interface for frame-level feature extraction.
37pub trait FeatureAdder<F> {
38    fn features_added(&self) -> usize {
39        self.get_column_headers().len()
40    }
41
42    fn get_column_headers(&self) -> &[&str];
43
44    fn add_features(
45        &self,
46        processor: &dyn ProcessorView,
47        frame: &boxcars::Frame,
48        frame_count: usize,
49        current_time: f32,
50        vector: &mut Vec<F>,
51    ) -> SubtrActorResult<()>;
52}
53
54/// Object-safe interface for frame-level features backed by the analysis graph.
55pub trait AnalysisFeatureAdder<F> {
56    fn features_added(&self) -> usize {
57        self.get_column_headers().len()
58    }
59
60    fn get_column_headers(&self) -> &[&str];
61
62    fn analysis_dependencies(&self) -> Vec<AnalysisDependency>;
63
64    fn add_features(
65        &self,
66        context: &AnalysisFeatureContext<'_>,
67        processor: &dyn ProcessorView,
68        frame: &boxcars::Frame,
69        frame_count: usize,
70        current_time: f32,
71        vector: &mut Vec<F>,
72    ) -> SubtrActorResult<()>;
73}
74
75/// Fixed-width analysis-backed feature extractor with compile-time column count validation.
76pub trait LengthCheckedAnalysisFeatureAdder<F, const N: usize> {
77    fn get_column_headers_array(&self) -> &[&str; N];
78
79    fn analysis_dependencies(&self) -> Vec<AnalysisDependency>;
80
81    fn get_features(
82        &self,
83        context: &AnalysisFeatureContext<'_>,
84        processor: &dyn ProcessorView,
85        frame: &boxcars::Frame,
86        frame_count: usize,
87        current_time: f32,
88    ) -> SubtrActorResult<[F; N]>;
89}
90
91/// Implements [`AnalysisFeatureAdder`] for a fixed-width analysis-backed type.
92#[macro_export]
93macro_rules! impl_analysis_feature_adder {
94    ($struct_name:ident) => {
95        impl<F: TryFrom<f32>> AnalysisFeatureAdder<F> for $struct_name<F>
96        where
97            <F as TryFrom<f32>>::Error: std::fmt::Debug,
98        {
99            fn add_features(
100                &self,
101                context: &AnalysisFeatureContext<'_>,
102                processor: &dyn ProcessorView,
103                frame: &boxcars::Frame,
104                frame_count: usize,
105                current_time: f32,
106                vector: &mut Vec<F>,
107            ) -> SubtrActorResult<()> {
108                Ok(vector.extend(self.get_features(
109                    context,
110                    processor,
111                    frame,
112                    frame_count,
113                    current_time,
114                )?))
115            }
116
117            fn get_column_headers(&self) -> &[&str] {
118                self.get_column_headers_array()
119            }
120
121            fn analysis_dependencies(&self) -> Vec<AnalysisDependency> {
122                LengthCheckedAnalysisFeatureAdder::analysis_dependencies(self)
123            }
124        }
125    };
126}
127
128/// Fixed-width feature extractor with compile-time column count validation.
129pub trait LengthCheckedFeatureAdder<F, const N: usize> {
130    fn get_column_headers_array(&self) -> &[&str; N];
131
132    fn get_features(
133        &self,
134        processor: &dyn ProcessorView,
135        frame: &boxcars::Frame,
136        frame_count: usize,
137        current_time: f32,
138    ) -> SubtrActorResult<[F; N]>;
139}
140
141/// Implements [`FeatureAdder`] for a type that already satisfies [`LengthCheckedFeatureAdder`].
142#[macro_export]
143macro_rules! impl_feature_adder {
144    ($struct_name:ident) => {
145        impl<F: TryFrom<f32>> FeatureAdder<F> for $struct_name<F>
146        where
147            <F as TryFrom<f32>>::Error: std::fmt::Debug,
148        {
149            fn add_features(
150                &self,
151                processor: &dyn ProcessorView,
152                frame: &boxcars::Frame,
153                frame_count: usize,
154                current_time: f32,
155                vector: &mut Vec<F>,
156            ) -> SubtrActorResult<()> {
157                Ok(
158                    vector.extend(self.get_features(
159                        processor,
160                        frame,
161                        frame_count,
162                        current_time,
163                    )?),
164                )
165            }
166
167            fn get_column_headers(&self) -> &[&str] {
168                self.get_column_headers_array()
169            }
170        }
171    };
172}
173
174/// Object-safe interface for per-player feature extraction.
175pub trait PlayerFeatureAdder<F> {
176    fn features_added(&self) -> usize {
177        self.get_column_headers().len()
178    }
179
180    fn get_column_headers(&self) -> &[&str];
181
182    fn add_features(
183        &self,
184        player_id: &PlayerId,
185        processor: &dyn ProcessorView,
186        frame: &boxcars::Frame,
187        frame_count: usize,
188        current_time: f32,
189        vector: &mut Vec<F>,
190    ) -> SubtrActorResult<()>;
191}
192
193/// Arguments supplied to object-safe per-player analysis feature adders.
194#[derive(Clone, Copy)]
195pub struct AnalysisPlayerFeatureInput<'a, 'ctx> {
196    pub context: &'a AnalysisFeatureContext<'ctx>,
197    pub player_id: &'a PlayerId,
198    pub processor: &'a dyn ProcessorView,
199    pub frame: &'a boxcars::Frame,
200    pub frame_count: usize,
201    pub current_time: f32,
202}
203
204/// Object-safe interface for per-player features backed by the analysis graph.
205pub trait AnalysisPlayerFeatureAdder<F> {
206    fn features_added(&self) -> usize {
207        self.get_column_headers().len()
208    }
209
210    fn get_column_headers(&self) -> &[&str];
211
212    fn analysis_dependencies(&self) -> Vec<AnalysisDependency>;
213
214    fn add_features(
215        &self,
216        input: AnalysisPlayerFeatureInput<'_, '_>,
217        vector: &mut Vec<F>,
218    ) -> SubtrActorResult<()>;
219}
220
221/// Fixed-width per-player analysis-backed feature extractor.
222pub trait LengthCheckedAnalysisPlayerFeatureAdder<F, const N: usize> {
223    fn get_column_headers_array(&self) -> &[&str; N];
224
225    fn analysis_dependencies(&self) -> Vec<AnalysisDependency>;
226
227    fn get_features(
228        &self,
229        context: &AnalysisFeatureContext<'_>,
230        player_id: &PlayerId,
231        processor: &dyn ProcessorView,
232        frame: &boxcars::Frame,
233        frame_count: usize,
234        current_time: f32,
235    ) -> SubtrActorResult<[F; N]>;
236}
237
238/// Implements [`AnalysisPlayerFeatureAdder`] for a fixed-width analysis-backed type.
239#[macro_export]
240macro_rules! impl_analysis_player_feature_adder {
241    ($struct_name:ident) => {
242        impl<F: TryFrom<f32>> AnalysisPlayerFeatureAdder<F> for $struct_name<F>
243        where
244            <F as TryFrom<f32>>::Error: std::fmt::Debug,
245        {
246            fn add_features(
247                &self,
248                input: AnalysisPlayerFeatureInput<'_, '_>,
249                vector: &mut Vec<F>,
250            ) -> SubtrActorResult<()> {
251                Ok(vector.extend(self.get_features(
252                    input.context,
253                    input.player_id,
254                    input.processor,
255                    input.frame,
256                    input.frame_count,
257                    input.current_time,
258                )?))
259            }
260
261            fn get_column_headers(&self) -> &[&str] {
262                self.get_column_headers_array()
263            }
264
265            fn analysis_dependencies(&self) -> Vec<AnalysisDependency> {
266                LengthCheckedAnalysisPlayerFeatureAdder::analysis_dependencies(self)
267            }
268        }
269    };
270}
271
272pub struct DynamicAnalysisFeatureAdder<F, G, const N: usize> {
273    get_features: G,
274    column_headers: &'static [&'static str; N],
275    dependencies: Vec<AnalysisDependency>,
276    _marker: PhantomData<F>,
277}
278
279impl<F, G, const N: usize> DynamicAnalysisFeatureAdder<F, G, N> {
280    pub fn new(
281        column_headers: &'static [&'static str; N],
282        dependencies: Vec<AnalysisDependency>,
283        get_features: G,
284    ) -> Self {
285        Self {
286            get_features,
287            column_headers,
288            dependencies,
289            _marker: PhantomData,
290        }
291    }
292}
293
294impl<F, G, const N: usize> AnalysisFeatureAdder<F> for DynamicAnalysisFeatureAdder<F, G, N>
295where
296    F: Send + Sync + 'static,
297    G: Fn(
298            &AnalysisFeatureContext<'_>,
299            &dyn ProcessorView,
300            &boxcars::Frame,
301            usize,
302            f32,
303        ) -> SubtrActorResult<[F; N]>
304        + Send
305        + Sync
306        + 'static,
307{
308    fn get_column_headers(&self) -> &[&str] {
309        self.column_headers.as_slice()
310    }
311
312    fn analysis_dependencies(&self) -> Vec<AnalysisDependency> {
313        self.dependencies.clone()
314    }
315
316    fn add_features(
317        &self,
318        context: &AnalysisFeatureContext<'_>,
319        processor: &dyn ProcessorView,
320        frame: &boxcars::Frame,
321        frame_count: usize,
322        current_time: f32,
323        vector: &mut Vec<F>,
324    ) -> SubtrActorResult<()> {
325        vector.extend((self.get_features)(
326            context,
327            processor,
328            frame,
329            frame_count,
330            current_time,
331        )?);
332        Ok(())
333    }
334}
335
336pub struct DynamicAnalysisPlayerFeatureAdder<F, G, const N: usize> {
337    get_features: G,
338    column_headers: &'static [&'static str; N],
339    dependencies: Vec<AnalysisDependency>,
340    _marker: PhantomData<F>,
341}
342
343impl<F, G, const N: usize> DynamicAnalysisPlayerFeatureAdder<F, G, N> {
344    pub fn new(
345        column_headers: &'static [&'static str; N],
346        dependencies: Vec<AnalysisDependency>,
347        get_features: G,
348    ) -> Self {
349        Self {
350            get_features,
351            column_headers,
352            dependencies,
353            _marker: PhantomData,
354        }
355    }
356}
357
358impl<F, G, const N: usize> AnalysisPlayerFeatureAdder<F>
359    for DynamicAnalysisPlayerFeatureAdder<F, G, N>
360where
361    F: Send + Sync + 'static,
362    G: Fn(
363            &AnalysisFeatureContext<'_>,
364            &PlayerId,
365            &dyn ProcessorView,
366            &boxcars::Frame,
367            usize,
368            f32,
369        ) -> SubtrActorResult<[F; N]>
370        + Send
371        + Sync
372        + 'static,
373{
374    fn get_column_headers(&self) -> &[&str] {
375        self.column_headers.as_slice()
376    }
377
378    fn analysis_dependencies(&self) -> Vec<AnalysisDependency> {
379        self.dependencies.clone()
380    }
381
382    fn add_features(
383        &self,
384        input: AnalysisPlayerFeatureInput<'_, '_>,
385        vector: &mut Vec<F>,
386    ) -> SubtrActorResult<()> {
387        vector.extend((self.get_features)(
388            input.context,
389            input.player_id,
390            input.processor,
391            input.frame,
392            input.frame_count,
393            input.current_time,
394        )?);
395        Ok(())
396    }
397}
398
399pub fn dynamic_analysis_feature_adder<F, G, const N: usize>(
400    column_headers: &'static [&'static str; N],
401    dependencies: Vec<AnalysisDependency>,
402    get_features: G,
403) -> Arc<dyn AnalysisFeatureAdder<F> + Send + Sync + 'static>
404where
405    F: Send + Sync + 'static,
406    G: Fn(
407            &AnalysisFeatureContext<'_>,
408            &dyn ProcessorView,
409            &boxcars::Frame,
410            usize,
411            f32,
412        ) -> SubtrActorResult<[F; N]>
413        + Send
414        + Sync
415        + 'static,
416{
417    Arc::new(DynamicAnalysisFeatureAdder::new(
418        column_headers,
419        dependencies,
420        get_features,
421    ))
422}
423
424pub fn dynamic_analysis_player_feature_adder<F, G, const N: usize>(
425    column_headers: &'static [&'static str; N],
426    dependencies: Vec<AnalysisDependency>,
427    get_features: G,
428) -> Arc<dyn AnalysisPlayerFeatureAdder<F> + Send + Sync + 'static>
429where
430    F: Send + Sync + 'static,
431    G: Fn(
432            &AnalysisFeatureContext<'_>,
433            &PlayerId,
434            &dyn ProcessorView,
435            &boxcars::Frame,
436            usize,
437            f32,
438        ) -> SubtrActorResult<[F; N]>
439        + Send
440        + Sync
441        + 'static,
442{
443    Arc::new(DynamicAnalysisPlayerFeatureAdder::new(
444        column_headers,
445        dependencies,
446        get_features,
447    ))
448}
449
450#[derive(Clone)]
451pub enum NDArrayFeatureAdder<F> {
452    Plain(Arc<dyn FeatureAdder<F> + Send + Sync>),
453    Analysis(Arc<dyn AnalysisFeatureAdder<F> + Send + Sync>),
454}
455
456impl<F> NDArrayFeatureAdder<F> {
457    pub fn plain(adder: Arc<dyn FeatureAdder<F> + Send + Sync>) -> Self {
458        Self::Plain(adder)
459    }
460
461    pub fn analysis(adder: Arc<dyn AnalysisFeatureAdder<F> + Send + Sync>) -> Self {
462        Self::Analysis(adder)
463    }
464
465    pub fn features_added(&self) -> usize {
466        match self {
467            Self::Plain(adder) => adder.features_added(),
468            Self::Analysis(adder) => adder.features_added(),
469        }
470    }
471
472    pub fn get_column_headers(&self) -> &[&str] {
473        match self {
474            Self::Plain(adder) => adder.get_column_headers(),
475            Self::Analysis(adder) => adder.get_column_headers(),
476        }
477    }
478
479    pub fn analysis_dependencies(&self) -> Vec<AnalysisDependency> {
480        match self {
481            Self::Plain(_) => Vec::new(),
482            Self::Analysis(adder) => adder.analysis_dependencies(),
483        }
484    }
485
486    pub fn is_analysis_backed(&self) -> bool {
487        matches!(self, Self::Analysis(_))
488    }
489}
490
491impl<F> From<Arc<dyn FeatureAdder<F> + Send + Sync>> for NDArrayFeatureAdder<F> {
492    fn from(adder: Arc<dyn FeatureAdder<F> + Send + Sync>) -> Self {
493        Self::plain(adder)
494    }
495}
496
497impl<F> From<Arc<dyn AnalysisFeatureAdder<F> + Send + Sync>> for NDArrayFeatureAdder<F> {
498    fn from(adder: Arc<dyn AnalysisFeatureAdder<F> + Send + Sync>) -> Self {
499        Self::analysis(adder)
500    }
501}
502
503pub type NDArrayFeatureAdders<F> = Vec<NDArrayFeatureAdder<F>>;
504
505#[derive(Clone)]
506pub enum NDArrayPlayerFeatureAdder<F> {
507    Plain(Arc<dyn PlayerFeatureAdder<F> + Send + Sync>),
508    Analysis(Arc<dyn AnalysisPlayerFeatureAdder<F> + Send + Sync>),
509}
510
511impl<F> NDArrayPlayerFeatureAdder<F> {
512    pub fn plain(adder: Arc<dyn PlayerFeatureAdder<F> + Send + Sync>) -> Self {
513        Self::Plain(adder)
514    }
515
516    pub fn analysis(adder: Arc<dyn AnalysisPlayerFeatureAdder<F> + Send + Sync>) -> Self {
517        Self::Analysis(adder)
518    }
519
520    pub fn features_added(&self) -> usize {
521        match self {
522            Self::Plain(adder) => adder.features_added(),
523            Self::Analysis(adder) => adder.features_added(),
524        }
525    }
526
527    pub fn get_column_headers(&self) -> &[&str] {
528        match self {
529            Self::Plain(adder) => adder.get_column_headers(),
530            Self::Analysis(adder) => adder.get_column_headers(),
531        }
532    }
533
534    pub fn analysis_dependencies(&self) -> Vec<AnalysisDependency> {
535        match self {
536            Self::Plain(_) => Vec::new(),
537            Self::Analysis(adder) => adder.analysis_dependencies(),
538        }
539    }
540
541    pub fn is_analysis_backed(&self) -> bool {
542        matches!(self, Self::Analysis(_))
543    }
544}
545
546impl<F> From<Arc<dyn PlayerFeatureAdder<F> + Send + Sync>> for NDArrayPlayerFeatureAdder<F> {
547    fn from(adder: Arc<dyn PlayerFeatureAdder<F> + Send + Sync>) -> Self {
548        Self::plain(adder)
549    }
550}
551
552impl<F> From<Arc<dyn AnalysisPlayerFeatureAdder<F> + Send + Sync>>
553    for NDArrayPlayerFeatureAdder<F>
554{
555    fn from(adder: Arc<dyn AnalysisPlayerFeatureAdder<F> + Send + Sync>) -> Self {
556        Self::analysis(adder)
557    }
558}
559
560pub type NDArrayPlayerFeatureAdders<F> = Vec<NDArrayPlayerFeatureAdder<F>>;
561
562/// Fixed-width per-player feature extractor with compile-time column count validation.
563pub trait LengthCheckedPlayerFeatureAdder<F, const N: usize> {
564    fn get_column_headers_array(&self) -> &[&str; N];
565
566    fn get_features(
567        &self,
568        player_id: &PlayerId,
569        processor: &dyn ProcessorView,
570        frame: &boxcars::Frame,
571        frame_count: usize,
572        current_time: f32,
573    ) -> SubtrActorResult<[F; N]>;
574}
575
576/// Implements [`PlayerFeatureAdder`] for a type that satisfies [`LengthCheckedPlayerFeatureAdder`].
577#[macro_export]
578macro_rules! impl_player_feature_adder {
579    ($struct_name:ident) => {
580        impl<F: TryFrom<f32>> PlayerFeatureAdder<F> for $struct_name<F>
581        where
582            <F as TryFrom<f32>>::Error: std::fmt::Debug,
583        {
584            fn add_features(
585                &self,
586                player_id: &PlayerId,
587                processor: &dyn ProcessorView,
588                frame: &boxcars::Frame,
589                frame_count: usize,
590                current_time: f32,
591                vector: &mut Vec<F>,
592            ) -> SubtrActorResult<()> {
593                Ok(vector.extend(self.get_features(
594                    player_id,
595                    processor,
596                    frame,
597                    frame_count,
598                    current_time,
599                )?))
600            }
601
602            fn get_column_headers(&self) -> &[&str] {
603                self.get_column_headers_array()
604            }
605        }
606    };
607}
608
609impl<G, F, const N: usize> FeatureAdder<F> for (G, &[&str; N])
610where
611    G: Fn(&dyn ProcessorView, &boxcars::Frame, usize, f32) -> SubtrActorResult<[F; N]>,
612{
613    fn add_features(
614        &self,
615        processor: &dyn ProcessorView,
616        frame: &boxcars::Frame,
617        frame_count: usize,
618        current_time: f32,
619        vector: &mut Vec<F>,
620    ) -> SubtrActorResult<()> {
621        vector.extend(self.0(processor, frame, frame_count, current_time)?);
622        Ok(())
623    }
624
625    fn get_column_headers(&self) -> &[&str] {
626        self.1.as_slice()
627    }
628}
629
630impl<G, F, const N: usize> PlayerFeatureAdder<F> for (G, &[&str; N])
631where
632    G: Fn(&PlayerId, &dyn ProcessorView, &boxcars::Frame, usize, f32) -> SubtrActorResult<[F; N]>,
633{
634    fn add_features(
635        &self,
636        player_id: &PlayerId,
637        processor: &dyn ProcessorView,
638        frame: &boxcars::Frame,
639        frame_count: usize,
640        current_time: f32,
641        vector: &mut Vec<F>,
642    ) -> SubtrActorResult<()> {
643        vector.extend(self.0(
644            player_id,
645            processor,
646            frame,
647            frame_count,
648            current_time,
649        )?);
650        Ok(())
651    }
652
653    fn get_column_headers(&self) -> &[&str] {
654        self.1.as_slice()
655    }
656}
657
658/// Declares a new global feature-adder type and wires it into the ndarray traits.
659#[macro_export]
660macro_rules! build_global_feature_adder {
661    ($struct_name:ident, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
662
663        #[derive(derive_new::new)]
664        pub struct $struct_name<F> {
665            _zero: std::marker::PhantomData<F>,
666        }
667
668        impl<F: Sync + Send + TryFrom<f32> + 'static> $struct_name<F> where
669            <F as TryFrom<f32>>::Error: std::fmt::Debug,
670        {
671            pub fn arc_new() -> std::sync::Arc<dyn FeatureAdder<F> + Send + Sync + 'static> {
672                std::sync::Arc::new(Self::new())
673            }
674        }
675
676        global_feature_adder!(
677            $struct_name,
678            $prop_getter,
679            $( $column_names ),*
680        );
681    }
682}
683
684/// Implements the ndarray feature-adder traits for an existing global feature type.
685#[macro_export]
686macro_rules! global_feature_adder {
687    ($struct_name:ident, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
688        macro_rules! _global_feature_adder {
689            ($count:ident) => {
690                impl<F: TryFrom<f32>> LengthCheckedFeatureAdder<F, $count> for $struct_name<F>
691                where
692                    <F as TryFrom<f32>>::Error: std::fmt::Debug,
693                {
694                    fn get_column_headers_array(&self) -> &[&str; $count] {
695                        &[$( $column_names ),*]
696                    }
697
698                    fn get_features(
699                        &self,
700                        processor: &dyn ProcessorView,
701                        frame: &boxcars::Frame,
702                        frame_count: usize,
703                        current_time: f32,
704                    ) -> SubtrActorResult<[F; $count]> {
705                        $prop_getter(self, processor, frame, frame_count, current_time)
706                    }
707                }
708
709                impl_feature_adder!($struct_name);
710            };
711        }
712        paste::paste! {
713            const [<$struct_name:snake:upper _LENGTH>]: usize = [$($column_names),*].len();
714            _global_feature_adder!([<$struct_name:snake:upper _LENGTH>]);
715        }
716    }
717}
718
719/// Declares a new analysis-backed global feature-adder type.
720#[macro_export]
721macro_rules! build_analysis_global_feature_adder {
722    ($struct_name:ident, $dependency_getter:expr, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
723
724        #[derive(derive_new::new)]
725        pub struct $struct_name<F> {
726            _zero: std::marker::PhantomData<F>,
727        }
728
729        impl<F: Sync + Send + TryFrom<f32> + 'static> $struct_name<F> where
730            <F as TryFrom<f32>>::Error: std::fmt::Debug,
731        {
732            pub fn arc_new() -> std::sync::Arc<dyn AnalysisFeatureAdder<F> + Send + Sync + 'static> {
733                std::sync::Arc::new(Self::new())
734            }
735        }
736
737        analysis_global_feature_adder!(
738            $struct_name,
739            $dependency_getter,
740            $prop_getter,
741            $( $column_names ),*
742        );
743    }
744}
745
746/// Implements the ndarray traits for an existing analysis-backed global feature type.
747#[macro_export]
748macro_rules! analysis_global_feature_adder {
749    ($struct_name:ident, $dependency_getter:expr, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
750        macro_rules! _analysis_global_feature_adder {
751            ($count:ident) => {
752                impl<F: TryFrom<f32>> LengthCheckedAnalysisFeatureAdder<F, $count> for $struct_name<F>
753                where
754                    <F as TryFrom<f32>>::Error: std::fmt::Debug,
755                {
756                    fn get_column_headers_array(&self) -> &[&str; $count] {
757                        &[$( $column_names ),*]
758                    }
759
760                    fn analysis_dependencies(&self) -> Vec<AnalysisDependency> {
761                        $dependency_getter(self)
762                    }
763
764                    fn get_features(
765                        &self,
766                        context: &AnalysisFeatureContext<'_>,
767                        processor: &dyn ProcessorView,
768                        frame: &boxcars::Frame,
769                        frame_count: usize,
770                        current_time: f32,
771                    ) -> SubtrActorResult<[F; $count]> {
772                        $prop_getter(self, context, processor, frame, frame_count, current_time)
773                    }
774                }
775
776                impl_analysis_feature_adder!($struct_name);
777            };
778        }
779        paste::paste! {
780            const [<$struct_name:snake:upper _LENGTH>]: usize = [$($column_names),*].len();
781            _analysis_global_feature_adder!([<$struct_name:snake:upper _LENGTH>]);
782        }
783    }
784}
785
786/// Declares a new per-player feature-adder type and wires it into the ndarray traits.
787#[macro_export]
788macro_rules! build_player_feature_adder {
789    ($struct_name:ident, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
790        #[derive(derive_new::new)]
791        pub struct $struct_name<F> {
792            _zero: std::marker::PhantomData<F>,
793        }
794
795        impl<F: Sync + Send + TryFrom<f32> + 'static> $struct_name<F> where
796            <F as TryFrom<f32>>::Error: std::fmt::Debug,
797        {
798            pub fn arc_new() -> std::sync::Arc<dyn PlayerFeatureAdder<F> + Send + Sync + 'static> {
799                std::sync::Arc::new(Self::new())
800            }
801        }
802
803        player_feature_adder!(
804            $struct_name,
805            $prop_getter,
806            $( $column_names ),*
807        );
808    }
809}
810
811/// Implements the ndarray feature-adder traits for an existing per-player feature type.
812#[macro_export]
813macro_rules! player_feature_adder {
814    ($struct_name:ident, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
815        macro_rules! _player_feature_adder {
816            ($count:ident) => {
817                impl<F: TryFrom<f32>> LengthCheckedPlayerFeatureAdder<F, $count> for $struct_name<F>
818                where
819                    <F as TryFrom<f32>>::Error: std::fmt::Debug,
820                {
821                    fn get_column_headers_array(&self) -> &[&str; $count] {
822                        &[$( $column_names ),*]
823                    }
824
825                    fn get_features(
826                        &self,
827                        player_id: &PlayerId,
828                        processor: &dyn ProcessorView,
829                        frame: &boxcars::Frame,
830                        frame_count: usize,
831                        current_time: f32,
832                    ) -> SubtrActorResult<[F; $count]> {
833                        $prop_getter(self, player_id, processor, frame, frame_count, current_time)
834                    }
835                }
836
837                impl_player_feature_adder!($struct_name);
838            };
839        }
840        paste::paste! {
841            const [<$struct_name:snake:upper _LENGTH>]: usize = [$($column_names),*].len();
842            _player_feature_adder!([<$struct_name:snake:upper _LENGTH>]);
843        }
844    }
845}
846
847/// Declares a new analysis-backed per-player feature-adder type.
848#[macro_export]
849macro_rules! build_analysis_player_feature_adder {
850    ($struct_name:ident, $dependency_getter:expr, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
851        #[derive(derive_new::new)]
852        pub struct $struct_name<F> {
853            _zero: std::marker::PhantomData<F>,
854        }
855
856        impl<F: Sync + Send + TryFrom<f32> + 'static> $struct_name<F> where
857            <F as TryFrom<f32>>::Error: std::fmt::Debug,
858        {
859            pub fn arc_new() -> std::sync::Arc<dyn AnalysisPlayerFeatureAdder<F> + Send + Sync + 'static> {
860                std::sync::Arc::new(Self::new())
861            }
862        }
863
864        analysis_player_feature_adder!(
865            $struct_name,
866            $dependency_getter,
867            $prop_getter,
868            $( $column_names ),*
869        );
870    }
871}
872
873/// Implements the ndarray traits for an existing analysis-backed per-player feature type.
874#[macro_export]
875macro_rules! analysis_player_feature_adder {
876    ($struct_name:ident, $dependency_getter:expr, $prop_getter:expr, $( $column_names:expr ),* $(,)?) => {
877        macro_rules! _analysis_player_feature_adder {
878            ($count:ident) => {
879                impl<F: TryFrom<f32>> LengthCheckedAnalysisPlayerFeatureAdder<F, $count> for $struct_name<F>
880                where
881                    <F as TryFrom<f32>>::Error: std::fmt::Debug,
882                {
883                    fn get_column_headers_array(&self) -> &[&str; $count] {
884                        &[$( $column_names ),*]
885                    }
886
887                    fn analysis_dependencies(&self) -> Vec<AnalysisDependency> {
888                        $dependency_getter(self)
889                    }
890
891                    fn get_features(
892                        &self,
893                        context: &AnalysisFeatureContext<'_>,
894                        player_id: &PlayerId,
895                        processor: &dyn ProcessorView,
896                        frame: &boxcars::Frame,
897                        frame_count: usize,
898                        current_time: f32,
899                    ) -> SubtrActorResult<[F; $count]> {
900                        $prop_getter(self, context, player_id, processor, frame, frame_count, current_time)
901                    }
902                }
903
904                impl_analysis_player_feature_adder!($struct_name);
905            };
906        }
907        paste::paste! {
908            const [<$struct_name:snake:upper _LENGTH>]: usize = [$($column_names),*].len();
909            _analysis_player_feature_adder!([<$struct_name:snake:upper _LENGTH>]);
910        }
911    }
912}
913
914/// Maps arbitrary conversion failures into a generic float-conversion error.
915pub fn convert_float_conversion_error<T>(_: T) -> SubtrActorError {
916    SubtrActorError::new(SubtrActorErrorVariant::FloatConversionError)
917}
918
919/// Converts a fixed list of values with a caller-supplied error mapper.
920#[macro_export]
921macro_rules! convert_all {
922    ($err:expr, $( $item:expr ),* $(,)?) => {{
923		Ok([
924			$( $item.try_into().map_err($err)? ),*
925		])
926	}};
927}
928
929/// Converts a fixed list of float-like values using [`convert_float_conversion_error`].
930#[macro_export]
931macro_rules! convert_all_floats {
932    ($( $item:expr ),* $(,)?) => {{
933        convert_all!(convert_float_conversion_error, $( $item ),*)
934    }};
935}