Skip to main content

howler_core/
ml.rs

1use crate::models::Sighting;
2use anyhow::Result;
3use chrono::{Datelike, Timelike};
4use linfa::prelude::*;
5use linfa_linear::LinearRegression;
6use linfa_trees::DecisionTree;
7use ndarray::{Array1, Array2, Axis};
8use rand::Rng;
9use serde::{Deserialize, Serialize};
10
11/// Predicted next location for a wolf
12#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct LocationPrediction {
14    /// Predicted latitude
15    pub latitude: f64,
16    /// Predicted longitude
17    pub longitude: f64,
18    /// Confidence score (0.0 - 1.0)
19    pub confidence: f64,
20    /// Prediction horizon in hours
21    pub horizon_hours: u32,
22}
23
24/// Behavior classification result
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
26pub enum BehaviorType {
27    /// Wolf is stationary (denning, resting)
28    Stationary,
29    /// Wolf is patrolling territory
30    Territorial,
31    /// Wolf is moving linearly (dispersal/migration)
32    Linear,
33    /// Wolf is moving randomly
34    Random,
35    /// Wolf is returning to a central location
36    CentralPlace,
37}
38
39/// Behavior prediction with metadata
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct BehaviorPrediction {
42    /// Predicted behavior type
43    pub behavior: BehaviorType,
44    /// Confidence score (0.0 - 1.0)
45    pub confidence: f64,
46    /// Features used for prediction
47    pub features: BehaviorFeatures,
48}
49
50/// Features extracted from sighting data for ML
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct BehaviorFeatures {
53    /// Average speed (km/h)
54    pub avg_speed_kmh: f64,
55    /// Speed variance
56    pub speed_variance: f64,
57    /// Turning angle variance (radians)
58    pub turning_angle_variance: f64,
59    /// Net displacement / total distance (straightness index)
60    pub straightness_index: f64,
61    /// Territory radius estimate (km)
62    pub territory_radius_km: f64,
63    /// Time since first sighting (hours)
64    pub time_span_hours: f64,
65    /// Number of sightings
66    pub num_sightings: usize,
67    /// Hour of day (0-23) - cyclical feature
68    pub hour_of_day_sin: f64,
69    pub hour_of_day_cos: f64,
70    /// Day of year (1-365) - cyclical feature
71    pub day_of_year_sin: f64,
72    pub day_of_year_cos: f64,
73}
74
75/// Activity prediction for a time period
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct ActivityPrediction {
78    /// Hour of day (0-23)
79    pub hour: u8,
80    /// Predicted probability of activity (0.0 - 1.0)
81    pub activity_probability: f64,
82    /// Expected number of sightings
83    pub expected_sightings: f64,
84}
85
86/// ML model for behavior prediction
87pub struct BehaviorModel {
88    /// Decision tree classifier for behavior type
89    classifier: Option<DecisionTree<f64, usize>>,
90    /// Linear regression for location prediction
91    location_model: Option<linfa_linear::FittedLinearRegression<f64>>,
92    /// Feature scaler
93    scaler_mean: Option<Array1<f64>>,
94    scaler_std: Option<Array1<f64>>,
95}
96
97impl Default for BehaviorModel {
98    fn default() -> Self {
99        Self::new()
100    }
101}
102
103impl BehaviorModel {
104    pub fn new() -> Self {
105        Self {
106            classifier: None,
107            location_model: None,
108            scaler_mean: None,
109            scaler_std: None,
110        }
111    }
112
113    /// Extract features from a sequence of sightings
114    pub fn extract_features(sightings: &[Sighting]) -> Result<BehaviorFeatures> {
115        if sightings.len() < 2 {
116            anyhow::bail!("Need at least 2 sightings for feature extraction");
117        }
118
119        let mut movements = Vec::new();
120        let mut speeds = Vec::new();
121        let mut bearings = Vec::new();
122
123        // Sort by time
124        let mut sorted = sightings.to_vec();
125        sorted.sort_by_key(|s| s.observed_on);
126
127        for window in sorted.windows(2) {
128            let from = &window[0];
129            let to = &window[1];
130
131            let distance = crate::movement::haversine_distance(
132                from.latitude,
133                from.longitude,
134                to.latitude,
135                to.longitude,
136            );
137            let bearing = crate::movement::calculate_bearing(
138                from.latitude,
139                from.longitude,
140                to.latitude,
141                to.longitude,
142            );
143            let duration = to
144                .observed_on
145                .signed_duration_since(from.observed_on)
146                .num_seconds() as f64
147                / 3600.0;
148
149            if duration > 0.0 {
150                let speed = distance / duration;
151                speeds.push(speed);
152                movements.push((distance, bearing));
153                bearings.push(bearing);
154            }
155        }
156
157        if speeds.is_empty() {
158            anyhow::bail!("No valid movements found");
159        }
160
161        // Calculate statistics
162        let avg_speed = speeds.iter().sum::<f64>() / speeds.len() as f64;
163        let speed_variance =
164            speeds.iter().map(|s| (s - avg_speed).powi(2)).sum::<f64>() / speeds.len() as f64;
165
166        // Turning angles
167        let mut turning_angles = Vec::new();
168        for i in 1..bearings.len() {
169            let diff = (bearings[i] - bearings[i - 1]).abs();
170            let angle = if diff > 180.0 { 360.0 - diff } else { diff };
171            turning_angles.push(angle.to_radians());
172        }
173        let turning_angle_variance = if turning_angles.len() > 1 {
174            let mean = turning_angles.iter().sum::<f64>() / turning_angles.len() as f64;
175            turning_angles
176                .iter()
177                .map(|a| (a - mean).powi(2))
178                .sum::<f64>()
179                / turning_angles.len() as f64
180        } else {
181            0.0
182        };
183
184        // Straightness index (net displacement / total path length)
185        let total_distance: f64 = movements.iter().map(|m| m.0).sum();
186        let net_displacement = if sorted.len() >= 2 {
187            let first = &sorted[0];
188            let last = &sorted[sorted.len() - 1];
189            crate::movement::haversine_distance(
190                first.latitude,
191                first.longitude,
192                last.latitude,
193                last.longitude,
194            )
195        } else {
196            0.0
197        };
198        let straightness_index = if total_distance > 0.0 {
199            net_displacement / total_distance
200        } else {
201            0.0
202        };
203
204        // Territory radius (max distance from centroid)
205        let centroid_lat = sorted.iter().map(|s| s.latitude).sum::<f64>() / sorted.len() as f64;
206        let centroid_lon = sorted.iter().map(|s| s.longitude).sum::<f64>() / sorted.len() as f64;
207        let territory_radius_km = sorted
208            .iter()
209            .map(|s| {
210                crate::movement::haversine_distance(
211                    s.latitude,
212                    s.longitude,
213                    centroid_lat,
214                    centroid_lon,
215                )
216            })
217            .fold(0.0, f64::max);
218
219        // Time span
220        let first_time = sorted.first().unwrap().observed_on;
221        let last_time = sorted.last().unwrap().observed_on;
222        let time_span_hours =
223            last_time.signed_duration_since(first_time).num_seconds() as f64 / 3600.0;
224
225        // Temporal features (using last sighting)
226        let last = sorted.last().unwrap();
227        let hour = last.observed_on.hour() as f64;
228        let day_of_year = last.observed_on.ordinal() as f64;
229
230        Ok(BehaviorFeatures {
231            avg_speed_kmh: avg_speed,
232            speed_variance,
233            turning_angle_variance,
234            straightness_index,
235            territory_radius_km,
236            time_span_hours,
237            num_sightings: sorted.len(),
238            hour_of_day_sin: (hour * 2.0 * std::f64::consts::PI / 24.0).sin(),
239            hour_of_day_cos: (hour * 2.0 * std::f64::consts::PI / 24.0).cos(),
240            day_of_year_sin: (day_of_year * 2.0 * std::f64::consts::PI / 365.0).sin(),
241            day_of_year_cos: (day_of_year * 2.0 * std::f64::consts::PI / 365.0).cos(),
242        })
243    }
244
245    /// Convert features to array for ML
246    fn features_to_array(features: &BehaviorFeatures) -> Array1<f64> {
247        Array1::from(vec![
248            features.avg_speed_kmh,
249            features.speed_variance,
250            features.turning_angle_variance,
251            features.straightness_index,
252            features.territory_radius_km,
253            features.time_span_hours,
254            features.num_sightings as f64,
255            features.hour_of_day_sin,
256            features.hour_of_day_cos,
257            features.day_of_year_sin,
258            features.day_of_year_cos,
259        ])
260    }
261
262    /// Standardize features (zero mean, unit variance)
263    fn standardize(&mut self, data: &mut Array2<f64>) {
264        let n_features = data.ncols();
265        let mut mean = Array1::zeros(n_features);
266        let mut std = Array1::zeros(n_features);
267
268        for j in 0..n_features {
269            let col = data.column(j);
270            mean[j] = col.mean().unwrap_or(0.0);
271            std[j] = col.std(0.0).max(1e-8);
272        }
273
274        for mut row in data.rows_mut() {
275            for j in 0..n_features {
276                row[j] = (row[j] - mean[j]) / std[j];
277            }
278        }
279
280        self.scaler_mean = Some(mean);
281        self.scaler_std = Some(std);
282    }
283
284    /// Apply standardization using fitted parameters
285    fn apply_scaling(&self, features: &mut Array1<f64>) {
286        if let (Some(mean), Some(std)) = (&self.scaler_mean, &self.scaler_std) {
287            for j in 0..features.len() {
288                features[j] = (features[j] - mean[j]) / std[j];
289            }
290        }
291    }
292
293    /// Train behavior classifier from labeled data
294    pub fn train_classifier(
295        &mut self,
296        training_data: &[(BehaviorFeatures, BehaviorType)],
297    ) -> Result<()> {
298        if training_data.is_empty() {
299            anyhow::bail!("No training data provided");
300        }
301
302        let n_samples = training_data.len();
303        let n_features = 11;
304        let mut x = Array2::zeros((n_samples, n_features));
305        let mut y = Array1::zeros(n_samples);
306
307        for (i, (features, label)) in training_data.iter().enumerate() {
308            let arr = Self::features_to_array(features);
309            for j in 0..n_features {
310                x[[i, j]] = arr[j];
311            }
312            y[i] = match label {
313                BehaviorType::Stationary => 0,
314                BehaviorType::Territorial => 1,
315                BehaviorType::Linear => 2,
316                BehaviorType::Random => 3,
317                BehaviorType::CentralPlace => 4,
318            };
319        }
320
321        self.standardize(&mut x);
322
323        let dataset = DatasetBase::new(x, y);
324        let model = DecisionTree::params()
325            .max_depth(Some(10))
326            .min_weight_split(2.0)
327            .min_weight_leaf(1.0)
328            .fit(&dataset)?;
329
330        self.classifier = Some(model);
331        Ok(())
332    }
333
334    /// Predict behavior type from features
335    pub fn predict_behavior(&self, features: &BehaviorFeatures) -> Result<BehaviorPrediction> {
336        let mut x = Self::features_to_array(features);
337        self.apply_scaling(&mut x);
338
339        let classifier = self
340            .classifier
341            .as_ref()
342            .ok_or_else(|| anyhow::anyhow!("Classifier not trained"))?;
343
344        let pred = classifier.predict(&x.view().insert_axis(Axis(0)));
345        let behavior = match pred[0] {
346            0 => BehaviorType::Stationary,
347            1 => BehaviorType::Territorial,
348            2 => BehaviorType::Linear,
349            3 => BehaviorType::Random,
350            4 => BehaviorType::CentralPlace,
351            _ => BehaviorType::Random,
352        };
353
354        Ok(BehaviorPrediction {
355            behavior,
356            confidence: 0.8,
357            features: features.clone(),
358        })
359    }
360
361    /// Train location predictor (linear regression for next position)
362    pub fn train_location_predictor(&mut self, sightings: &[Sighting]) -> Result<()> {
363        if sightings.len() < 3 {
364            anyhow::bail!("Need at least 3 sightings for location prediction");
365        }
366
367        let mut sorted = sightings.to_vec();
368        sorted.sort_by_key(|s| s.observed_on);
369
370        let n = sorted.len() - 1;
371        let mut x = Array2::zeros((n, 3));
372        let mut y_lat = Array1::zeros(n);
373        let mut y_lon = Array1::zeros(n);
374
375        for i in 0..n {
376            let dt = sorted[i + 1]
377                .observed_on
378                .signed_duration_since(sorted[i].observed_on)
379                .num_seconds() as f64
380                / 3600.0;
381            x[[i, 0]] = dt;
382            x[[i, 1]] = sorted[i].latitude;
383            x[[i, 2]] = sorted[i].longitude;
384            y_lat[i] = sorted[i + 1].latitude;
385            y_lon[i] = sorted[i + 1].longitude;
386        }
387
388        // Train latitude model
389        let dataset_lat = DatasetBase::new(x, y_lat);
390        let model = LinearRegression::default().fit(&dataset_lat)?;
391        self.location_model = Some(model);
392
393        Ok(())
394    }
395
396    /// Predict next location
397    pub fn predict_next_location(
398        &self,
399        sightings: &[Sighting],
400        horizon_hours: u32,
401    ) -> Result<LocationPrediction> {
402        if sightings.len() < 2 {
403            anyhow::bail!("Need at least 2 sightings");
404        }
405
406        let mut sorted = sightings.to_vec();
407        sorted.sort_by_key(|s| s.observed_on);
408
409        let last = sorted.last().unwrap();
410        let prev = &sorted[sorted.len() - 2];
411
412        let dt = last
413            .observed_on
414            .signed_duration_since(prev.observed_on)
415            .num_seconds() as f64
416            / 3600.0;
417
418        // Simple linear extrapolation based on last movement
419        let bearing = crate::movement::calculate_bearing(
420            prev.latitude,
421            prev.longitude,
422            last.latitude,
423            last.longitude,
424        );
425        let distance = crate::movement::haversine_distance(
426            prev.latitude,
427            prev.longitude,
428            last.latitude,
429            last.longitude,
430        );
431        let speed = if dt > 0.0 { distance / dt } else { 0.0 };
432
433        let pred_distance = speed * horizon_hours as f64;
434
435        // Destination point calculation
436        let lat1 = last.latitude.to_radians();
437        let lon1 = last.longitude.to_radians();
438        let brng = bearing.to_radians();
439        let d = pred_distance / 6371.0;
440
441        let lat2 = (lat1.sin() * d.cos() + lat1.cos() * d.sin() * brng.cos()).asin();
442        let lon2 =
443            lon1 + (brng.sin() * d.cos() * lat1.cos()).atan2(d.cos() - lat1.sin() * lat2.sin());
444
445        let confidence = (1.0 / (1.0 + horizon_hours as f64 * 0.1)).min(0.95);
446
447        Ok(LocationPrediction {
448            latitude: lat2.to_degrees(),
449            longitude: lon2.to_degrees(),
450            confidence,
451            horizon_hours,
452        })
453    }
454}
455
456/// Predict activity patterns by hour of day
457pub fn predict_activity_pattern(sightings: &[Sighting]) -> Vec<ActivityPrediction> {
458    let mut hourly_counts = [0usize; 24];
459    let mut total = 0;
460
461    for s in sightings {
462        let hour = s.observed_on.hour() as usize;
463        hourly_counts[hour] += 1;
464        total += 1;
465    }
466
467    if total == 0 {
468        return vec![];
469    }
470
471    // Simple smoothing with prior
472    let mut predictions = Vec::new();
473    for (hour, &count) in hourly_counts.iter().enumerate() {
474        let count = count as f64;
475        let probability = (count + 0.5) / (total as f64 + 12.0); // Laplace smoothing
476        predictions.push(ActivityPrediction {
477            hour: hour as u8,
478            activity_probability: probability,
479            expected_sightings: probability * (total as f64 / 24.0),
480        });
481    }
482
483    predictions
484}
485
486/// Generate synthetic training data for behavior classification
487pub fn generate_synthetic_training_data() -> Vec<(BehaviorFeatures, BehaviorType)> {
488    let mut data = Vec::new();
489    let mut rng = rand::thread_rng();
490
491    // Stationary: low speed, low variance, low straightness
492    for _ in 0..50 {
493        data.push((
494            BehaviorFeatures {
495                avg_speed_kmh: rng.gen_range(0.0..0.5),
496                speed_variance: rng.gen_range(0.0..0.1),
497                turning_angle_variance: rng.gen_range(0.0..1.0),
498                straightness_index: rng.gen_range(0.0..0.3),
499                territory_radius_km: rng.gen_range(0.0..2.0),
500                time_span_hours: rng.gen_range(1.0..100.0),
501                num_sightings: rng.gen_range(5..20),
502                hour_of_day_sin: rng.gen_range(-1.0..1.0),
503                hour_of_day_cos: rng.gen_range(-1.0..1.0),
504                day_of_year_sin: rng.gen_range(-1.0..1.0),
505                day_of_year_cos: rng.gen_range(-1.0..1.0),
506            },
507            BehaviorType::Stationary,
508        ));
509    }
510
511    // Territorial: moderate speed, high turning variance, low straightness, small territory
512    for _ in 0..50 {
513        data.push((
514            BehaviorFeatures {
515                avg_speed_kmh: rng.gen_range(1.0..5.0),
516                speed_variance: rng.gen_range(0.5..3.0),
517                turning_angle_variance: rng.gen_range(1.0..3.0),
518                straightness_index: rng.gen_range(0.1..0.4),
519                territory_radius_km: rng.gen_range(5.0..20.0),
520                time_span_hours: rng.gen_range(24.0..500.0),
521                num_sightings: rng.gen_range(10..50),
522                hour_of_day_sin: rng.gen_range(-1.0..1.0),
523                hour_of_day_cos: rng.gen_range(-1.0..1.0),
524                day_of_year_sin: rng.gen_range(-1.0..1.0),
525                day_of_year_cos: rng.gen_range(-1.0..1.0),
526            },
527            BehaviorType::Territorial,
528        ));
529    }
530
531    // Linear: high speed, low turning variance, high straightness
532    for _ in 0..50 {
533        data.push((
534            BehaviorFeatures {
535                avg_speed_kmh: rng.gen_range(3.0..10.0),
536                speed_variance: rng.gen_range(0.5..2.0),
537                turning_angle_variance: rng.gen_range(0.0..0.5),
538                straightness_index: rng.gen_range(0.7..1.0),
539                territory_radius_km: rng.gen_range(50.0..500.0),
540                time_span_hours: rng.gen_range(10.0..200.0),
541                num_sightings: rng.gen_range(5..30),
542                hour_of_day_sin: rng.gen_range(-1.0..1.0),
543                hour_of_day_cos: rng.gen_range(-1.0..1.0),
544                day_of_year_sin: rng.gen_range(-1.0..1.0),
545                day_of_year_cos: rng.gen_range(-1.0..1.0),
546            },
547            BehaviorType::Linear,
548        ));
549    }
550
551    // Random: moderate speed, high turning variance, low straightness
552    for _ in 0..50 {
553        data.push((
554            BehaviorFeatures {
555                avg_speed_kmh: rng.gen_range(0.5..3.0),
556                speed_variance: rng.gen_range(1.0..5.0),
557                turning_angle_variance: rng.gen_range(2.0..4.0),
558                straightness_index: rng.gen_range(0.0..0.3),
559                territory_radius_km: rng.gen_range(10.0..100.0),
560                time_span_hours: rng.gen_range(10.0..300.0),
561                num_sightings: rng.gen_range(5..40),
562                hour_of_day_sin: rng.gen_range(-1.0..1.0),
563                hour_of_day_cos: rng.gen_range(-1.0..1.0),
564                day_of_year_sin: rng.gen_range(-1.0..1.0),
565                day_of_year_cos: rng.gen_range(-1.0..1.0),
566            },
567            BehaviorType::Random,
568        ));
569    }
570
571    // Central place: moderate speed, low turning variance, moderate straightness, returning to center
572    for _ in 0..50 {
573        data.push((
574            BehaviorFeatures {
575                avg_speed_kmh: rng.gen_range(1.0..4.0),
576                speed_variance: rng.gen_range(0.5..2.0),
577                turning_angle_variance: rng.gen_range(0.5..1.5),
578                straightness_index: rng.gen_range(0.3..0.6),
579                territory_radius_km: rng.gen_range(5.0..30.0),
580                time_span_hours: rng.gen_range(24.0..400.0),
581                num_sightings: rng.gen_range(10..60),
582                hour_of_day_sin: rng.gen_range(-1.0..1.0),
583                hour_of_day_cos: rng.gen_range(-1.0..1.0),
584                day_of_year_sin: rng.gen_range(-1.0..1.0),
585                day_of_year_cos: rng.gen_range(-1.0..1.0),
586            },
587            BehaviorType::CentralPlace,
588        ));
589    }
590
591    data
592}
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597    use crate::models::{Sighting, Source};
598    use chrono::Utc;
599
600    fn create_test_sightings() -> Vec<Sighting> {
601        let base_time = Utc::now();
602        vec![
603            Sighting {
604                id: Some(1),
605                species: "Canis lupus".to_string(),
606                scientific_name: Some("Canis lupus".to_string()),
607                latitude: 45.0,
608                longitude: -122.0,
609                observed_on: base_time - chrono::Duration::hours(6),
610                source: Source::GBIF,
611                source_id: "test_1".to_string(),
612                details: None,
613            },
614            Sighting {
615                id: Some(2),
616                species: "Canis lupus".to_string(),
617                scientific_name: Some("Canis lupus".to_string()),
618                latitude: 45.1,
619                longitude: -122.1,
620                observed_on: base_time - chrono::Duration::hours(3),
621                source: Source::GBIF,
622                source_id: "test_2".to_string(),
623                details: None,
624            },
625            Sighting {
626                id: Some(3),
627                species: "Canis lupus".to_string(),
628                scientific_name: Some("Canis lupus".to_string()),
629                latitude: 45.2,
630                longitude: -122.2,
631                observed_on: base_time,
632                source: Source::GBIF,
633                source_id: "test_3".to_string(),
634                details: None,
635            },
636        ]
637    }
638
639    #[test]
640    fn test_extract_features() {
641        let sightings = create_test_sightings();
642        let features = BehaviorModel::extract_features(&sightings).unwrap();
643
644        assert!(features.avg_speed_kmh >= 0.0);
645        assert_eq!(features.num_sightings, 3);
646        assert!(features.time_span_hours > 0.0);
647    }
648
649    #[test]
650    fn test_predict_activity_pattern() {
651        let sightings = create_test_sightings();
652        let predictions = predict_activity_pattern(&sightings);
653
654        assert_eq!(predictions.len(), 24);
655        let total_prob: f64 = predictions.iter().map(|p| p.activity_probability).sum();
656        assert!((total_prob - 1.0).abs() < 0.1);
657    }
658
659    #[test]
660    fn test_generate_synthetic_training_data() {
661        let data = generate_synthetic_training_data();
662        assert_eq!(data.len(), 250);
663
664        let mut class_counts = std::collections::HashMap::new();
665        for (_, label) in &data {
666            *class_counts.entry(label.clone()).or_insert(0) += 1;
667        }
668        assert_eq!(class_counts.len(), 5);
669    }
670
671    #[test]
672    fn test_behavior_model_train_and_predict() {
673        let training_data = generate_synthetic_training_data();
674        let mut model = BehaviorModel::new();
675        model.train_classifier(&training_data).unwrap();
676
677        let features = BehaviorFeatures {
678            avg_speed_kmh: 0.1,
679            speed_variance: 0.01,
680            turning_angle_variance: 0.5,
681            straightness_index: 0.1,
682            territory_radius_km: 0.5,
683            time_span_hours: 50.0,
684            num_sightings: 10,
685            hour_of_day_sin: 0.0,
686            hour_of_day_cos: 1.0,
687            day_of_year_sin: 0.0,
688            day_of_year_cos: 1.0,
689        };
690
691        let prediction = model.predict_behavior(&features).unwrap();
692        assert!(prediction.confidence > 0.0);
693    }
694
695    #[test]
696    fn test_location_prediction() {
697        let sightings = create_test_sightings();
698        let model = BehaviorModel::new();
699        let prediction = model.predict_next_location(&sightings, 1).unwrap();
700
701        assert!(prediction.latitude >= -90.0 && prediction.latitude <= 90.0);
702        assert!(prediction.longitude >= -180.0 && prediction.longitude <= 180.0);
703        assert!(prediction.confidence > 0.0 && prediction.confidence <= 1.0);
704        assert_eq!(prediction.horizon_hours, 1);
705    }
706}