1use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9
10#[cfg(feature = "serde")]
11extern crate serde_yaml;
12
13#[derive(Debug, Clone)]
19pub struct VisualPipelineBuilder {
20 pub component_library: ComponentLibrary,
22 pub pipeline_canvas: PipelineCanvas,
24 pub code_generator: VisualCodeGenerator,
26 pub validator: PipelineValidator,
28 pub export_manager: PipelineExportManager,
30 pub settings: VisualBuilderSettings,
32}
33
34impl VisualPipelineBuilder {
35 pub fn new() -> Self {
37 Self {
38 component_library: ComponentLibrary::new(),
39 pipeline_canvas: PipelineCanvas::new(),
40 code_generator: VisualCodeGenerator::new(),
41 validator: PipelineValidator::new(),
42 export_manager: PipelineExportManager::new(),
43 settings: VisualBuilderSettings::default(),
44 }
45 }
46
47 pub fn generate_web_interface(&self) -> crate::error::Result<WebInterface> {
52 let html_template = self.generate_html_interface()?;
53 let javascript_code = self.generate_javascript_logic()?;
54 let css_styling = self.generate_css_styles()?;
55 let component_definitions = self.generate_component_definitions()?;
56
57 Ok(WebInterface {
58 html_template,
59 javascript_code,
60 css_styling,
61 component_definitions,
62 api_endpoints: self.generate_api_endpoints()?,
63 websocket_handlers: self.generate_websocket_handlers()?,
64 })
65 }
66
67 pub fn build_pipeline_from_visual(
72 &self,
73 visual_config: &VisualPipelineConfig,
74 ) -> crate::error::Result<GeneratedPipeline> {
75 let validation_result = self.validator.validate_visual_pipeline(visual_config)?;
77 if !validation_result.is_valid {
78 return Err(crate::error::SklearsError::ValidationError(format!(
79 "Pipeline validation failed: {}",
80 validation_result.error_message.unwrap_or_default()
81 )));
82 }
83
84 let dsl_code = self
86 .code_generator
87 .generate_dsl_from_visual(visual_config)?;
88
89 let rust_code = self
91 .code_generator
92 .generate_rust_implementation(visual_config)?;
93
94 let documentation = self.generate_pipeline_documentation(visual_config)?;
96
97 let dependencies = self.analyze_dependencies(visual_config)?;
99 let performance_hints = self.generate_performance_hints(visual_config)?;
100 let test_code = self.generate_test_code(visual_config)?;
101
102 Ok(GeneratedPipeline {
103 name: visual_config.name.clone(),
104 dsl_code,
105 rust_code,
106 documentation,
107 dependencies,
108 performance_hints,
109 test_code,
110 metadata: visual_config.metadata.clone(),
111 })
112 }
113
114 pub fn import_pipeline(
119 &mut self,
120 import_data: &PipelineImportData,
121 ) -> crate::error::Result<VisualPipelineConfig> {
122 match import_data.format {
123 ImportFormat::Json => self.import_from_json(&import_data.content),
124 ImportFormat::Yaml => self.import_from_yaml(&import_data.content),
125 ImportFormat::SklearnPipeline => self.import_from_sklearn(&import_data.content),
126 ImportFormat::TorchScript => self.import_from_torch(&import_data.content),
127 ImportFormat::DslMacro => self.import_from_dsl_macro(&import_data.content),
128 ImportFormat::OnnxModel => self.import_from_onnx(&import_data.content),
129 }
130 }
131
132 pub fn export_pipeline(
137 &self,
138 config: &VisualPipelineConfig,
139 format: ExportFormat,
140 ) -> crate::error::Result<String> {
141 self.export_manager.export(config, format)
142 }
143
144 pub fn validate_pipeline(
146 &self,
147 config: &VisualPipelineConfig,
148 ) -> crate::error::Result<ValidationResult> {
149 self.validator.validate_visual_pipeline(config)
150 }
151
152 pub fn optimize_pipeline(
157 &self,
158 config: &VisualPipelineConfig,
159 ) -> crate::error::Result<VisualPipelineConfig> {
160 let mut optimized_config = config.clone();
161
162 self.optimize_components(&mut optimized_config)?;
164
165 self.optimize_data_flow(&mut optimized_config)?;
167
168 self.optimize_resource_usage(&mut optimized_config)?;
170
171 Ok(optimized_config)
172 }
173
174 pub fn get_component_templates(&self) -> &Vec<ComponentTemplate> {
176 &self.component_library.templates
177 }
178
179 pub fn add_custom_component(&mut self, component: ComponentDef) -> crate::error::Result<()> {
181 self.component_library.add_custom_component(component)
182 }
183
184 fn generate_html_interface(&self) -> crate::error::Result<String> {
186 Ok(r#"<!DOCTYPE html>
187<html lang="en">
188<head>
189 <meta charset="UTF-8">
190 <title>Visual Pipeline Builder</title>
191 <link rel="stylesheet" href="visual_builder.css">
192</head>
193<body>
194 <div id="pipeline-canvas"></div>
195 <div id="component-library"></div>
196 <script src="visual_builder.js"></script>
197</body>
198</html>"#
199 .to_string())
200 }
201
202 fn generate_javascript_logic(&self) -> crate::error::Result<String> {
204 Ok(r#"
205// Visual Pipeline Builder JavaScript
206console.log('Visual Pipeline Builder loaded');
207"#
208 .to_string())
209 }
210
211 fn generate_css_styles(&self) -> crate::error::Result<String> {
213 Ok(r#"
214/* Visual Pipeline Builder CSS */
215#pipeline-canvas { width: 100%; height: 600px; border: 1px solid #ccc; }
216"#
217 .to_string())
218 }
219
220 fn generate_component_definitions(&self) -> crate::error::Result<String> {
222 let definitions = serde_json::to_string_pretty(&self.component_library)?;
223 Ok(definitions)
224 }
225
226 fn generate_api_endpoints(&self) -> crate::error::Result<Vec<ApiEndpoint>> {
228 Ok(vec![
229 ApiEndpoint {
230 path: "/api/components".to_string(),
231 method: "GET".to_string(),
232 description: "Get available components".to_string(),
233 },
234 ApiEndpoint {
235 path: "/api/pipeline/validate".to_string(),
236 method: "POST".to_string(),
237 description: "Validate pipeline configuration".to_string(),
238 },
239 ApiEndpoint {
240 path: "/api/pipeline/generate".to_string(),
241 method: "POST".to_string(),
242 description: "Generate code from visual configuration".to_string(),
243 },
244 ApiEndpoint {
245 path: "/api/pipeline/export".to_string(),
246 method: "POST".to_string(),
247 description: "Export pipeline to various formats".to_string(),
248 },
249 ])
250 }
251
252 fn generate_websocket_handlers(&self) -> crate::error::Result<Vec<WebSocketHandler>> {
254 Ok(vec![
255 WebSocketHandler {
256 event: "component_added".to_string(),
257 description: "Handle component addition to canvas".to_string(),
258 },
259 WebSocketHandler {
260 event: "component_moved".to_string(),
261 description: "Handle component movement on canvas".to_string(),
262 },
263 WebSocketHandler {
264 event: "connection_created".to_string(),
265 description: "Handle connection between components".to_string(),
266 },
267 ])
268 }
269
270 fn generate_pipeline_documentation(
272 &self,
273 config: &VisualPipelineConfig,
274 ) -> crate::error::Result<PipelineDocumentation> {
275 Ok(PipelineDocumentation {
276 overview: format!("Pipeline: {}", config.name),
277 components: config
278 .components
279 .iter()
280 .map(|c| format!("- {}: {}", c.name, c.description))
281 .collect(),
282 data_flow: self.describe_data_flow(config)?,
283 performance_notes: self.generate_performance_notes(config)?,
284 usage_examples: self.generate_usage_examples(config)?,
285 })
286 }
287
288 fn analyze_dependencies(
290 &self,
291 config: &VisualPipelineConfig,
292 ) -> crate::error::Result<Vec<String>> {
293 let mut dependencies = HashSet::new();
294
295 for component in &config.components {
296 dependencies.extend(component.dependencies.iter().cloned());
297 }
298
299 Ok(dependencies.into_iter().collect())
300 }
301
302 fn generate_performance_hints(
304 &self,
305 config: &VisualPipelineConfig,
306 ) -> crate::error::Result<Vec<PerformanceHint>> {
307 let mut hints = Vec::new();
308
309 if config.components.len() > 10 {
311 hints.push(PerformanceHint {
312 category: "Complexity".to_string(),
313 message:
314 "Consider breaking down complex pipelines into smaller, reusable components"
315 .to_string(),
316 severity: "Medium".to_string(),
317 });
318 }
319
320 if self.can_parallelize(config)? {
322 hints.push(PerformanceHint {
323 category: "Parallelization".to_string(),
324 message: "This pipeline can benefit from parallel execution".to_string(),
325 severity: "Low".to_string(),
326 });
327 }
328
329 Ok(hints)
330 }
331
332 fn generate_test_code(&self, config: &VisualPipelineConfig) -> crate::error::Result<String> {
334 let test_template = format!(
335 r#"
336#[allow(non_snake_case)]
337#[cfg(test)]
338mod tests {{
339 use super::*;
340
341 #[test]
342 fn test_{}_pipeline() {{
343 let pipeline = {}Pipeline::new().expect("Failed to create pipeline");
344 // Add your test logic here
345 assert!(true);
346 }}
347
348 #[test]
349 fn test_{}_pipeline_with_sample_data() {{
350 let pipeline = {}Pipeline::new().expect("Failed to create pipeline");
351 // Add sample data testing here
352 assert!(true);
353 }}
354}}
355 "#,
356 config.name.to_lowercase(),
357 config.name,
358 config.name.to_lowercase(),
359 config.name
360 );
361
362 Ok(test_template)
363 }
364
365 fn optimize_components(&self, config: &mut VisualPipelineConfig) -> crate::error::Result<()> {
367 for component in &mut config.components {
369 if component.component_type == "preprocessing" {
370 component
371 .properties
372 .insert("use_simd".to_string(), "true".to_string());
373 }
374 }
375 Ok(())
376 }
377
378 fn optimize_data_flow(&self, _config: &mut VisualPipelineConfig) -> crate::error::Result<()> {
379 Ok(())
381 }
382
383 fn optimize_resource_usage(
384 &self,
385 _config: &mut VisualPipelineConfig,
386 ) -> crate::error::Result<()> {
387 Ok(())
389 }
390
391 fn describe_data_flow(&self, config: &VisualPipelineConfig) -> crate::error::Result<String> {
392 Ok(format!(
393 "Data flows through {} components",
394 config.components.len()
395 ))
396 }
397
398 fn generate_performance_notes(
399 &self,
400 config: &VisualPipelineConfig,
401 ) -> crate::error::Result<String> {
402 Ok(format!(
403 "Pipeline with {} components",
404 config.components.len()
405 ))
406 }
407
408 fn generate_usage_examples(
409 &self,
410 config: &VisualPipelineConfig,
411 ) -> crate::error::Result<Vec<String>> {
412 Ok(vec![format!(
413 "let pipeline = {}Pipeline::new()?;",
414 config.name
415 )])
416 }
417
418 fn can_parallelize(&self, config: &VisualPipelineConfig) -> crate::error::Result<bool> {
419 Ok(config.components.len() > 2)
420 }
421
422 fn import_from_json(&self, content: &str) -> crate::error::Result<VisualPipelineConfig> {
424 Ok(serde_json::from_str(content)?)
425 }
426
427 fn import_from_yaml(&self, content: &str) -> crate::error::Result<VisualPipelineConfig> {
428 #[cfg(feature = "serde")]
429 {
430 serde_yaml::from_str(content)
431 .map_err(|e| crate::error::SklearsError::SerializationError(e.to_string()))
432 }
433 #[cfg(not(feature = "serde"))]
434 {
435 let _ = content;
436 Err(crate::error::SklearsError::NotImplemented(
437 "YAML import requires the 'serde' feature".to_string(),
438 ))
439 }
440 }
441
442 fn import_from_sklearn(&self, _content: &str) -> crate::error::Result<VisualPipelineConfig> {
443 Err(crate::error::SklearsError::NotImplemented(
445 "sklearn import not yet implemented".to_string(),
446 ))
447 }
448
449 fn import_from_torch(&self, _content: &str) -> crate::error::Result<VisualPipelineConfig> {
450 Err(crate::error::SklearsError::NotImplemented(
452 "PyTorch import not yet implemented".to_string(),
453 ))
454 }
455
456 fn import_from_dsl_macro(&self, _content: &str) -> crate::error::Result<VisualPipelineConfig> {
457 Err(crate::error::SklearsError::NotImplemented(
459 "DSL macro import not yet implemented".to_string(),
460 ))
461 }
462
463 fn import_from_onnx(&self, _content: &str) -> crate::error::Result<VisualPipelineConfig> {
464 Err(crate::error::SklearsError::NotImplemented(
466 "ONNX import not yet implemented".to_string(),
467 ))
468 }
469}
470
471impl Default for VisualPipelineBuilder {
472 fn default() -> Self {
473 Self::new()
474 }
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize)]
479pub struct VisualPipelineConfig {
480 pub name: String,
482 pub description: String,
484 pub components: Vec<ComponentInstance>,
486 pub connections: Vec<ComponentConnection>,
488 pub layout: CanvasLayout,
490 pub metadata: HashMap<String, String>,
492 pub settings: PipelineSettings,
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize)]
498pub struct ComponentInstance {
499 pub id: String,
501 pub name: String,
503 pub component_type: String,
505 pub properties: HashMap<String, String>,
507 pub position: ComponentPosition,
509 pub dependencies: Vec<String>,
511 pub description: String,
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize)]
517pub struct ComponentConnection {
518 pub from_component: String,
520 pub from_port: String,
522 pub to_component: String,
524 pub to_port: String,
526 pub data_type: String,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct ComponentPosition {
533 pub x: f64,
534 pub y: f64,
535 pub width: f64,
536 pub height: f64,
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize)]
541pub struct CanvasLayout {
542 pub width: f64,
543 pub height: f64,
544 pub zoom_level: f64,
545 pub grid_enabled: bool,
546 pub snap_to_grid: bool,
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
551pub struct PipelineSettings {
552 pub parallel_execution: bool,
553 pub cache_intermediate_results: bool,
554 pub enable_gpu: bool,
555 pub memory_limit_mb: Option<usize>,
556 pub timeout_seconds: Option<u64>,
557}
558
559impl Default for PipelineSettings {
560 fn default() -> Self {
561 Self {
562 parallel_execution: false,
563 cache_intermediate_results: true,
564 enable_gpu: false,
565 memory_limit_mb: None,
566 timeout_seconds: None,
567 }
568 }
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize)]
573pub struct ComponentLibrary {
574 pub templates: Vec<ComponentTemplate>,
576 pub custom_components: Vec<ComponentDef>,
578 pub categories: Vec<ComponentCategory>,
580}
581
582impl Default for ComponentLibrary {
583 fn default() -> Self {
584 Self::new()
585 }
586}
587
588impl ComponentLibrary {
589 pub fn new() -> Self {
590 Self {
591 templates: Self::create_default_templates(),
592 custom_components: Vec::new(),
593 categories: Self::create_default_categories(),
594 }
595 }
596
597 pub fn add_custom_component(&mut self, component: ComponentDef) -> crate::error::Result<()> {
598 self.custom_components.push(component);
599 Ok(())
600 }
601
602 fn create_default_templates() -> Vec<ComponentTemplate> {
603 vec![
604 ComponentTemplate {
605 id: "data_loader".to_string(),
606 name: "Data Loader".to_string(),
607 category: "input".to_string(),
608 description: "Load data from various sources".to_string(),
609 input_ports: vec![],
610 output_ports: vec!["data".to_string()],
611 properties: HashMap::new(),
612 },
613 ComponentTemplate {
614 id: "scaler".to_string(),
615 name: "Feature Scaler".to_string(),
616 category: "preprocessing".to_string(),
617 description: "Scale features to a standard range".to_string(),
618 input_ports: vec!["data".to_string()],
619 output_ports: vec!["scaled_data".to_string()],
620 properties: HashMap::new(),
621 },
622 ComponentTemplate {
623 id: "random_forest".to_string(),
624 name: "Random Forest".to_string(),
625 category: "model".to_string(),
626 description: "Random Forest classifier/regressor".to_string(),
627 input_ports: vec!["features".to_string(), "labels".to_string()],
628 output_ports: vec!["predictions".to_string()],
629 properties: HashMap::new(),
630 },
631 ]
632 }
633
634 fn create_default_categories() -> Vec<ComponentCategory> {
635 vec![
636 ComponentCategory {
637 id: "input".to_string(),
638 name: "Data Input".to_string(),
639 description: "Components for loading and importing data".to_string(),
640 },
641 ComponentCategory {
642 id: "preprocessing".to_string(),
643 name: "Preprocessing".to_string(),
644 description: "Data cleaning and transformation components".to_string(),
645 },
646 ComponentCategory {
647 id: "model".to_string(),
648 name: "Models".to_string(),
649 description: "Machine learning models and algorithms".to_string(),
650 },
651 ComponentCategory {
652 id: "output".to_string(),
653 name: "Output".to_string(),
654 description: "Components for saving and exporting results".to_string(),
655 },
656 ]
657 }
658}
659
660#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct ComponentTemplate {
663 pub id: String,
664 pub name: String,
665 pub category: String,
666 pub description: String,
667 pub input_ports: Vec<String>,
668 pub output_ports: Vec<String>,
669 pub properties: HashMap<String, String>,
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize)]
674pub struct ComponentDef {
675 pub name: String,
676 pub description: String,
677 pub category: String,
678 pub implementation: String,
679 pub dependencies: Vec<String>,
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize)]
684pub struct ComponentCategory {
685 pub id: String,
686 pub name: String,
687 pub description: String,
688}
689
690#[derive(Debug, Clone)]
692pub struct PipelineCanvas {
693 pub width: u32,
694 pub height: u32,
695 pub zoom_level: f64,
696 pub grid_enabled: bool,
697 pub components: Vec<ComponentInstance>,
698 pub connections: Vec<ComponentConnection>,
699}
700
701impl Default for PipelineCanvas {
702 fn default() -> Self {
703 Self::new()
704 }
705}
706
707impl PipelineCanvas {
708 pub fn new() -> Self {
709 Self {
710 width: 1920,
711 height: 1080,
712 zoom_level: 1.0,
713 grid_enabled: true,
714 components: Vec::new(),
715 connections: Vec::new(),
716 }
717 }
718}
719
720#[derive(Debug, Clone)]
722pub struct VisualCodeGenerator {
723 pub settings: CodeGenerationSettings,
724}
725
726impl VisualCodeGenerator {
727 pub fn new() -> Self {
728 Self {
729 settings: CodeGenerationSettings::default(),
730 }
731 }
732
733 pub fn generate_dsl_from_visual(
734 &self,
735 config: &VisualPipelineConfig,
736 ) -> crate::error::Result<String> {
737 Ok(format!("// Generated DSL for {}", config.name))
739 }
740
741 pub fn generate_rust_implementation(
742 &self,
743 config: &VisualPipelineConfig,
744 ) -> crate::error::Result<String> {
745 Ok(format!("// Generated Rust code for {}", config.name))
747 }
748}
749
750impl Default for VisualCodeGenerator {
751 fn default() -> Self {
752 Self::new()
753 }
754}
755
756#[derive(Debug, Clone)]
758pub struct CodeGenerationSettings {
759 pub optimize_for_performance: bool,
760 pub include_documentation: bool,
761 pub generate_tests: bool,
762 pub target_rust_edition: String,
763}
764
765impl Default for CodeGenerationSettings {
766 fn default() -> Self {
767 Self {
768 optimize_for_performance: true,
769 include_documentation: true,
770 generate_tests: true,
771 target_rust_edition: "2021".to_string(),
772 }
773 }
774}
775
776#[derive(Debug, Clone)]
781pub struct VisualBuilderSettings {
782 pub auto_save: bool,
783 pub collaborative_editing: bool,
784 pub theme: String,
785 pub grid_size: u32,
786}
787
788impl Default for VisualBuilderSettings {
789 fn default() -> Self {
790 Self {
791 auto_save: true,
792 collaborative_editing: false,
793 theme: "dark".to_string(),
794 grid_size: 20,
795 }
796 }
797}
798
799#[derive(Debug, Clone)]
801pub struct PipelineValidator;
802
803impl Default for PipelineValidator {
804 fn default() -> Self {
805 Self
806 }
807}
808
809impl PipelineValidator {
810 pub fn new() -> Self {
811 Self
812 }
813 pub fn validate_visual_pipeline(
814 &self,
815 _config: &VisualPipelineConfig,
816 ) -> crate::error::Result<ValidationResult> {
817 Ok(ValidationResult {
818 is_valid: true,
819 error_message: None,
820 })
821 }
822}
823
824#[derive(Debug, Clone)]
825pub struct PipelineExportManager;
826
827impl Default for PipelineExportManager {
828 fn default() -> Self {
829 Self
830 }
831}
832
833impl PipelineExportManager {
834 pub fn new() -> Self {
835 Self
836 }
837 pub fn export(
838 &self,
839 _config: &VisualPipelineConfig,
840 _format: ExportFormat,
841 ) -> crate::error::Result<String> {
842 Ok("// Exported code".to_string())
843 }
844}
845
846#[derive(Debug, Clone)]
848pub struct ValidationResult {
849 pub is_valid: bool,
850 pub error_message: Option<String>,
851}
852
853#[derive(Debug, Clone)]
854pub struct GeneratedPipeline {
855 pub name: String,
856 pub dsl_code: String,
857 pub rust_code: String,
858 pub documentation: PipelineDocumentation,
859 pub dependencies: Vec<String>,
860 pub performance_hints: Vec<PerformanceHint>,
861 pub test_code: String,
862 pub metadata: HashMap<String, String>,
863}
864
865#[derive(Debug, Clone)]
866pub struct PipelineDocumentation {
867 pub overview: String,
868 pub components: Vec<String>,
869 pub data_flow: String,
870 pub performance_notes: String,
871 pub usage_examples: Vec<String>,
872}
873
874#[derive(Debug, Clone)]
875pub struct PerformanceHint {
876 pub category: String,
877 pub message: String,
878 pub severity: String,
879}
880
881#[derive(Debug, Clone)]
882pub struct PipelineImportData {
883 pub format: ImportFormat,
884 pub content: String,
885}
886
887#[derive(Debug, Clone)]
888pub struct WebInterface {
889 pub html_template: String,
890 pub javascript_code: String,
891 pub css_styling: String,
892 pub component_definitions: String,
893 pub api_endpoints: Vec<ApiEndpoint>,
894 pub websocket_handlers: Vec<WebSocketHandler>,
895}
896
897#[derive(Debug, Clone)]
898pub struct ApiEndpoint {
899 pub path: String,
900 pub method: String,
901 pub description: String,
902}
903
904#[derive(Debug, Clone)]
905pub struct WebSocketHandler {
906 pub event: String,
907 pub description: String,
908}
909
910#[derive(Debug, Clone)]
911pub enum ImportFormat {
912 Json,
913 Yaml,
914 SklearnPipeline,
915 TorchScript,
916 DslMacro,
917 OnnxModel,
918}
919
920#[derive(Debug, Clone)]
921pub enum ExportFormat {
922 Json,
923 Yaml,
924 RustCode,
925 PythonCode,
926 DslMacro,
927}
928
929#[allow(non_snake_case)]
930#[cfg(test)]
931mod tests {
932 use super::*;
933
934 #[test]
935 fn test_visual_builder_creation() {
936 let builder = VisualPipelineBuilder::new();
937 assert_eq!(builder.component_library.templates.len(), 3);
938 }
939
940 #[test]
941 fn test_component_library_default() {
942 let library = ComponentLibrary::new();
943 assert!(!library.templates.is_empty());
944 assert!(!library.categories.is_empty());
945 }
946
947 #[test]
948 fn test_pipeline_canvas_default() {
949 let canvas = PipelineCanvas::new();
950 assert_eq!(canvas.width, 1920);
951 assert_eq!(canvas.height, 1080);
952 assert_eq!(canvas.zoom_level, 1.0);
953 }
954}