Deepanshu042 commited on
Commit
8728572
Β·
verified Β·
1 Parent(s): 162ee74

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +349 -167
app.py CHANGED
@@ -1,181 +1,363 @@
1
- import pandas as pd
2
- import numpy as np
3
- from sklearn.model_selection import train_test_split
4
- from sklearn.ensemble import RandomForestClassifier
5
- from sklearn.metrics import accuracy_score, classification_report
6
  import joblib
 
 
7
 
8
- def create_realistic_water_data():
9
- """
10
- Create realistic water quality dataset with clear safe/unsafe patterns
11
- """
12
- np.random.seed(42)
13
- n_samples = 2000
14
-
15
- # Create 50% safe and 50% unsafe samples
16
- n_safe = n_samples // 2
17
- n_unsafe = n_samples - n_safe
18
-
19
- data = []
20
- labels = []
21
-
22
- # Generate SAFE water samples
23
- print(f"🟒 Generating {n_safe} SAFE water samples...")
24
- for i in range(n_safe):
25
- # Safe water characteristics
26
- ph = np.random.normal(7.2, 0.5) # Around neutral pH
27
- ph = np.clip(ph, 6.5, 8.5) # Keep in safe range
28
 
29
- hardness = np.random.uniform(80, 250) # Moderate hardness
 
 
 
 
 
 
30
 
31
- solids = np.random.uniform(50, 400) # Low dissolved solids
32
-
33
- chloramines = np.random.uniform(0.5, 3.5) # Low chloramines
 
 
 
 
 
 
 
 
34
 
35
- data.append([ph, hardness, solids, chloramines])
36
- labels.append(1) # Safe
37
-
38
- # Generate UNSAFE water samples
39
- print(f"πŸ”΄ Generating {n_unsafe} UNSAFE water samples...")
40
- for i in range(n_unsafe):
41
- # Randomly choose what makes it unsafe
42
- unsafe_type = np.random.choice(['acidic', 'alkaline', 'high_solids', 'high_chloramines', 'multiple'])
43
-
44
- if unsafe_type == 'acidic':
45
- ph = np.random.uniform(3.0, 6.0) # Too acidic
46
- hardness = np.random.uniform(50, 300)
47
- solids = np.random.uniform(100, 1000)
48
- chloramines = np.random.uniform(1.0, 6.0)
49
 
50
- elif unsafe_type == 'alkaline':
51
- ph = np.random.uniform(9.0, 11.0) # Too alkaline
52
- hardness = np.random.uniform(100, 350)
53
- solids = np.random.uniform(200, 1500)
54
- chloramines = np.random.uniform(2.0, 7.0)
55
 
56
- elif unsafe_type == 'high_solids':
57
- ph = np.random.uniform(6.0, 8.0)
58
- hardness = np.random.uniform(150, 400)
59
- solids = np.random.uniform(1000, 60000) # Very high solids
60
- chloramines = np.random.uniform(2.0, 8.0)
61
 
62
- elif unsafe_type == 'high_chloramines':
63
- ph = np.random.uniform(6.5, 8.0)
64
- hardness = np.random.uniform(100, 300)
65
- solids = np.random.uniform(200, 800)
66
- chloramines = np.random.uniform(8.0, 15.0) # High chloramines
 
 
 
 
 
 
 
67
 
68
- else: # multiple issues
69
- ph = np.random.uniform(4.0, 5.5) # Acidic
70
- hardness = np.random.uniform(30, 100) # Low hardness
71
- solids = np.random.uniform(2000, 50000) # High solids
72
- chloramines = np.random.uniform(8.0, 13.0) # High chloramines
73
-
74
- data.append([ph, hardness, solids, chloramines])
75
- labels.append(0) # Unsafe
76
-
77
- # Convert to DataFrame
78
- df = pd.DataFrame(data, columns=['pH', 'Hardness', 'Solids', 'Chloramines'])
79
- df['Potability'] = labels
80
-
81
- # Shuffle the data
82
- df = df.sample(frac=1, random_state=42).reset_index(drop=True)
83
-
84
- return df
 
 
 
 
 
 
 
 
85
 
86
- def train_improved_model():
87
- """
88
- Train an improved 4-feature water quality model
89
- """
90
- print("🧬 Training Improved BioSentinel Model")
91
- print("=" * 50)
92
-
93
- # Create dataset
94
- df = create_realistic_water_data()
95
- print(f"βœ… Dataset created: {len(df)} samples")
96
- print(f"🎯 Safe samples: {sum(df['Potability'] == 1)}")
97
- print(f"🎯 Unsafe samples: {sum(df['Potability'] == 0)}")
98
-
99
- # Show sample statistics
100
- print("\nπŸ“Š Dataset Statistics:")
101
- print(df.describe())
102
-
103
- # Prepare features and target
104
- feature_columns = ['pH', 'Hardness', 'Solids', 'Chloramines']
105
- X = df[feature_columns]
106
- y = df['Potability']
107
-
108
- # Split data
109
- X_train, X_test, y_train, y_test = train_test_split(
110
- X, y, test_size=0.2, random_state=42, stratify=y
111
- )
112
-
113
- print(f"\nπŸ”„ Training set: {len(X_train)} samples")
114
- print(f"πŸ”„ Test set: {len(X_test)} samples")
115
-
116
- # Train improved model
117
- print("\nπŸ€– Training Random Forest model...")
118
- model = RandomForestClassifier(
119
- n_estimators=200, # More trees
120
- random_state=42,
121
- max_depth=15,
122
- min_samples_split=10,
123
- min_samples_leaf=5,
124
- class_weight='balanced' # Handle any class imbalance
125
- )
126
-
127
- model.fit(X_train, y_train)
128
-
129
- # Evaluate model
130
- print("\nπŸ§ͺ Evaluating model...")
131
- train_pred = model.predict(X_train)
132
- test_pred = model.predict(X_test)
133
-
134
- train_acc = accuracy_score(y_train, train_pred)
135
- test_acc = accuracy_score(y_test, test_pred)
136
-
137
- print(f"πŸ“ˆ Training Accuracy: {train_acc:.3f}")
138
- print(f"πŸ“ˆ Test Accuracy: {test_acc:.3f}")
139
-
140
- # Feature importance
141
- print(f"\nπŸ“Š Feature Importance:")
142
- for feature, importance in zip(feature_columns, model.feature_importances_):
143
- print(f" {feature}: {importance:.3f}")
144
-
145
- # Test with specific examples
146
- print(f"\nπŸ§ͺ Testing specific examples:")
147
- test_cases = [
148
- ([7.0, 200, 300, 3.0], "Good water"),
149
- ([4.5, 80, 45000, 11.0], "Bad water"),
150
- ([8.8, 320, 800, 4.5], "Borderline water")
151
- ]
152
-
153
- for features, description in test_cases:
154
- X_test = np.array([features])
155
- pred = model.predict(X_test)[0]
156
- if hasattr(model, 'predict_proba'):
157
- proba = model.predict_proba(X_test)[0]
158
- conf = max(proba)
159
- print(f" {description}: {'SAFE' if pred==1 else 'UNSAFE'} (confidence: {conf:.2%})")
160
  else:
161
- print(f" {description}: {'SAFE' if pred==1 else 'UNSAFE'}")
162
-
163
- # Save model
164
- print(f"\nπŸ’Ύ Saving model...")
165
- joblib.dump(model, 'model.joblib')
166
- print("βœ… Model saved as 'model.joblib'")
167
-
168
- # Verify saved model
169
- print(f"\nπŸ”„ Testing saved model...")
170
- loaded_model = joblib.load('model.joblib')
171
- test_input = np.array([[7.2, 180, 250, 2.5]])
172
- prediction = loaded_model.predict(test_input)[0]
173
- print(f"βœ… Saved model test: {'SAFE' if prediction==1 else 'UNSAFE'}")
174
-
175
- print(f"\nπŸŽ‰ Improved BioSentinel model ready!")
176
- print(f"πŸ’‘ This model should give varied predictions based on input parameters")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
177
 
178
- return model
 
 
 
179
 
 
180
  if __name__ == "__main__":
181
- model = train_improved_model()
 
1
+ # app.py - Fixed for 4-feature BioSentinel Model
2
+ import gradio as gr
 
 
 
3
  import joblib
4
+ import numpy as np
5
+ from typing import Tuple
6
 
7
+ class BioSentinelPredictor:
8
+ def __init__(self):
9
+ try:
10
+ self.model = joblib.load("model.joblib")
11
+ self.model_loaded = True
12
+ print(f"βœ… Model loaded successfully!")
13
+ except Exception as e:
14
+ print(f"❌ Error loading model: {e}")
15
+ self.model_loaded = False
 
 
 
 
 
 
 
 
 
 
 
16
 
17
+ # Only 4 features to match your trained model
18
+ self.feature_columns = [
19
+ "pH",
20
+ "Hardness",
21
+ "Solids",
22
+ "Chloramines"
23
+ ]
24
 
25
+ # Realistic ranges for water quality parameters
26
+ self.feature_ranges = {
27
+ "pH": (3.0, 9.5, 7.0),
28
+ "Hardness": (47.0, 323.0, 196.0),
29
+ "Solids": (320.0, 61227.0, 22000.0),
30
+ "Chloramines": (0.35, 13.13, 7.0)
31
+ }
32
+
33
+ def predict(self, ph, hardness, solids, chloramines) -> Tuple[str, str, str]:
34
+ if not self.model_loaded:
35
+ return "❌ Model Error", "Could not load the trained model. Please check if model.joblib exists.", ""
36
 
37
+ try:
38
+ # Create input array with exactly 4 features (matching your model)
39
+ features = np.array([[ph, hardness, solids, chloramines]])
 
 
 
 
 
 
 
 
 
 
 
40
 
41
+ # Debug: Print input values
42
+ print(f"πŸ” Input values: pH={ph}, Hardness={hardness}, Solids={solids}, Chloramines={chloramines}")
 
 
 
43
 
44
+ # Make prediction
45
+ prediction = self.model.predict(features)[0]
46
+ print(f"🎯 Model prediction: {prediction}")
 
 
47
 
48
+ # Get confidence score if available
49
+ confidence_text = ""
50
+ proba_values = None
51
+ if hasattr(self.model, 'predict_proba'):
52
+ try:
53
+ proba = self.model.predict_proba(features)[0]
54
+ proba_values = proba
55
+ confidence = max(proba)
56
+ confidence_text = f"\nConfidence: {confidence:.1%}"
57
+ print(f"πŸ“Š Probabilities: {proba}")
58
+ except Exception as e:
59
+ print(f"Confidence error: {e}")
60
 
61
+ # Create dynamic detailed analysis based on actual parameters
62
+ detailed_analysis = self._create_dynamic_analysis(ph, hardness, solids, chloramines, prediction, proba_values)
63
+
64
+ # Interpret results with more nuanced responses
65
+ if prediction == 1:
66
+ result = "βœ… SAFE TO DRINK"
67
+ base_description = "Water quality analysis indicates this sample is generally SAFE for consumption."
68
+ else:
69
+ result = "⚠️ NOT SAFE"
70
+ base_description = "Water quality analysis indicates potential safety concerns with this sample."
71
+
72
+ # Add specific concerns based on parameters
73
+ concerns = self._identify_concerns(ph, hardness, solids, chloramines)
74
+
75
+ final_description = f"{base_description}\n\n{detailed_analysis}{confidence_text}"
76
+
77
+ # Parameter breakdown
78
+ feature_analysis = self._create_parameter_breakdown(ph, hardness, solids, chloramines)
79
+
80
+ return result, final_description, feature_analysis
81
+
82
+ except Exception as e:
83
+ error_msg = str(e)
84
+ print(f"❌ Prediction error: {error_msg}")
85
+ return "❌ Prediction Failed", f"Error during prediction: {error_msg}", "Please check your input values and try again."
86
 
87
+ def _create_dynamic_analysis(self, ph, hardness, solids, chloramines, prediction, proba_values):
88
+ """Create dynamic analysis based on actual parameter values"""
89
+
90
+ analysis_parts = []
91
+
92
+ # pH Analysis
93
+ if ph < 6.5:
94
+ analysis_parts.append("πŸ”΄ **pH too acidic** - May cause corrosion and health issues")
95
+ elif ph > 8.5:
96
+ analysis_parts.append("πŸ”΄ **pH too alkaline** - May cause scaling and taste issues")
97
+ elif 6.5 <= ph <= 8.5:
98
+ analysis_parts.append("🟒 **pH within safe range** - Good for consumption")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  else:
100
+ analysis_parts.append("🟑 **pH borderline** - Monitor closely")
101
+
102
+ # Hardness Analysis
103
+ if hardness > 300:
104
+ analysis_parts.append("🟑 **High mineral content** - Very hard water")
105
+ elif hardness > 180:
106
+ analysis_parts.append("🟒 **Moderate mineral content** - Moderately hard water")
107
+ else:
108
+ analysis_parts.append("🟒 **Low to moderate hardness** - Acceptable levels")
109
+
110
+ # Solids Analysis
111
+ if solids > 1000:
112
+ analysis_parts.append("πŸ”΄ **High dissolved solids** - Exceeds WHO guidelines")
113
+ elif solids > 500:
114
+ analysis_parts.append("🟑 **Elevated dissolved solids** - Above recommended levels")
115
+ else:
116
+ analysis_parts.append("🟒 **Dissolved solids acceptable** - Within safe range")
117
+
118
+ # Chloramines Analysis
119
+ if chloramines > 4.0:
120
+ analysis_parts.append("πŸ”΄ **High chloramines** - Exceeds EPA maximum")
121
+ elif chloramines > 2.0:
122
+ analysis_parts.append("🟑 **Moderate chloramines** - Acceptable but elevated")
123
+ else:
124
+ analysis_parts.append("🟒 **Low chloramines** - Within safe limits")
125
+
126
+ # Risk level based on prediction and confidence
127
+ if prediction == 1:
128
+ if proba_values is not None and max(proba_values) > 0.8:
129
+ risk_text = "🟒 **Low Risk** - High confidence in safety assessment"
130
+ elif proba_values is not None and max(proba_values) > 0.6:
131
+ risk_text = "🟑 **Moderate Confidence** - Generally safe but monitor"
132
+ else:
133
+ risk_text = "🟑 **Low Confidence** - Additional testing recommended"
134
+ else:
135
+ if proba_values is not None and max(proba_values) > 0.8:
136
+ risk_text = "πŸ”΄ **High Risk** - Strong indication of safety concerns"
137
+ elif proba_values is not None and max(proba_values) > 0.6:
138
+ risk_text = "🟑 **Moderate Risk** - Some safety concerns identified"
139
+ else:
140
+ risk_text = "🟑 **Uncertain Risk** - Requires professional testing"
141
+
142
+ # Recommendations
143
+ if prediction == 0:
144
+ if ph < 6.5 or ph > 8.5:
145
+ recommendations = "πŸ’‘ **Recommended Actions:** pH adjustment, professional water treatment"
146
+ elif solids > 1000:
147
+ recommendations = "πŸ’‘ **Recommended Actions:** Filtration system, reverse osmosis treatment"
148
+ elif chloramines > 4.0:
149
+ recommendations = "πŸ’‘ **Recommended Actions:** Carbon filtration, contact water utility"
150
+ else:
151
+ recommendations = "πŸ’‘ **Recommended Actions:** Professional water testing, consider treatment options"
152
+ else:
153
+ recommendations = "πŸ’‘ **Recommended Actions:** Regular monitoring, maintain current water source quality"
154
+
155
+ return f"{risk_text}\n\n" + "\n".join(analysis_parts) + f"\n\n{recommendations}"
156
+
157
+ def _identify_concerns(self, ph, hardness, solids, chloramines):
158
+ """Identify specific parameter concerns"""
159
+ concerns = []
160
+
161
+ if ph < 6.5 or ph > 8.5:
162
+ concerns.append("pH out of safe range")
163
+ if hardness > 300:
164
+ concerns.append("very hard water")
165
+ if solids > 500:
166
+ concerns.append("high dissolved solids")
167
+ if chloramines > 4.0:
168
+ concerns.append("excessive chloramines")
169
+
170
+ return concerns
171
+
172
+ def _create_parameter_breakdown(self, ph, hardness, solids, chloramines) -> str:
173
+ """Create detailed parameter analysis"""
174
+
175
+ analysis = "πŸ“Š **Parameter Analysis:**\n\n"
176
+ values = [ph, hardness, solids, chloramines]
177
+ units = ["", "(mg/L)", "(ppm)", "(ppm)"]
178
+
179
+ # WHO/EPA standard ranges for reference
180
+ safe_ranges = {
181
+ "pH": (6.5, 8.5),
182
+ "Hardness": (0, 300),
183
+ "Solids": (0, 500), # WHO guideline for TDS
184
+ "Chloramines": (0, 4.0) # EPA maximum
185
+ }
186
+
187
+ for i, (feature, value) in enumerate(zip(self.feature_columns, values)):
188
+ unit = units[i]
189
+
190
+ # Determine status based on WHO/EPA guidelines
191
+ if feature in safe_ranges:
192
+ min_safe, max_safe = safe_ranges[feature]
193
+ if min_safe <= value <= max_safe:
194
+ status = "βœ… Within Safe Range"
195
+ color = "🟒"
196
+ elif value < min_safe:
197
+ status = "⚠️ Below Safe Range"
198
+ color = "🟑"
199
+ else:
200
+ status = "❌ Above Safe Range"
201
+ color = "πŸ”΄"
202
+ else:
203
+ status = "ℹ️ Measured"
204
+ color = "βšͺ"
205
+
206
+ analysis += f"{color} **{feature}**: {value:.2f} {unit} - {status}\n"
207
+
208
+ analysis += "\nπŸ“‹ **Guidelines Used**: WHO & EPA Water Quality Standards"
209
+ return analysis
210
+
211
+ # Initialize the predictor
212
+ predictor = BioSentinelPredictor()
213
+
214
+ def create_interface():
215
+ with gr.Blocks(
216
+ title="🧬 BioSentinel Water Quality Predictor",
217
+ theme=gr.themes.Soft(),
218
+ css="""
219
+ .gradio-container {
220
+ max-width: 1200px !important;
221
+ }
222
+ """
223
+ ) as interface:
224
+
225
+ # Header
226
+ gr.Markdown("""
227
+ # 🧬 BioSentinel - Water Quality Predictor
228
+
229
+ **AI-Powered Water Safety Assessment System**
230
+
231
+ Enter water quality parameters below for instant safety analysis. This system uses machine learning
232
+ to evaluate water potability based on critical physicochemical properties.
233
+ """)
234
+
235
+ # Main interface
236
+ with gr.Row():
237
+ # Input Section
238
+ with gr.Column(scale=1):
239
+ gr.Markdown("### πŸ”¬ Water Quality Parameters")
240
+
241
+ ph_input = gr.Slider(
242
+ minimum=3.0, maximum=9.5, value=7.0, step=0.1,
243
+ label="🌊 pH Level",
244
+ info="Acidity/Alkalinity measure (6.5-8.5 ideal)"
245
+ )
246
+
247
+ hardness_input = gr.Slider(
248
+ minimum=47.0, maximum=323.0, value=196.0, step=1.0,
249
+ label="πŸ’Ž Hardness (mg/L)",
250
+ info="Calcium & magnesium content (0-300 acceptable)"
251
+ )
252
+
253
+ solids_input = gr.Slider(
254
+ minimum=320.0, maximum=61227.0, value=22000.0, step=100.0,
255
+ label="βšͺ Total Dissolved Solids (ppm)",
256
+ info="Dissolved mineral content (<500 ideal)"
257
+ )
258
+
259
+ chloramines_input = gr.Slider(
260
+ minimum=0.35, maximum=13.13, value=7.0, step=0.1,
261
+ label="πŸ§ͺ Chloramines (ppm)",
262
+ info="Disinfection byproducts (<4.0 safe)"
263
+ )
264
+
265
+ # Analysis Button
266
+ predict_button = gr.Button(
267
+ "πŸ” Analyze Water Quality",
268
+ variant="primary",
269
+ size="lg",
270
+ scale=1
271
+ )
272
+
273
+ # Results Section
274
+ with gr.Column(scale=1):
275
+ gr.Markdown("### 🎯 Analysis Results")
276
+
277
+ # Main result
278
+ result_output = gr.Textbox(
279
+ label="Safety Assessment",
280
+ placeholder="Click 'Analyze Water Quality' to get results...",
281
+ lines=2,
282
+ max_lines=2
283
+ )
284
+
285
+ # Detailed analysis
286
+ description_output = gr.Textbox(
287
+ label="Detailed Analysis",
288
+ placeholder="Detailed safety analysis will appear here...",
289
+ lines=4,
290
+ max_lines=6
291
+ )
292
+
293
+ # Parameter breakdown
294
+ feature_output = gr.Textbox(
295
+ label="Parameter Breakdown",
296
+ placeholder="Individual parameter analysis will show here...",
297
+ lines=8,
298
+ max_lines=12
299
+ )
300
+
301
+ # Connect prediction function
302
+ inputs = [ph_input, hardness_input, solids_input, chloramines_input]
303
+ outputs = [result_output, description_output, feature_output]
304
+
305
+ predict_button.click(
306
+ fn=predictor.predict,
307
+ inputs=inputs,
308
+ outputs=outputs
309
+ )
310
+
311
+ # Quick Examples Section
312
+ with gr.Row():
313
+ gr.Markdown("### πŸ“‹ Quick Test Examples")
314
+
315
+ with gr.Row():
316
+ safe_btn = gr.Button("βœ… Safe Water Sample", variant="secondary")
317
+ unsafe_btn = gr.Button("❌ Unsafe Water Sample", variant="secondary")
318
+ neutral_btn = gr.Button("βš–οΈ Borderline Sample", variant="secondary")
319
+
320
+ # Example functions with more varied data
321
+ def load_safe_example():
322
+ return [7.2, 150.0, 250.0, 2.5] # Really good quality water
323
+
324
+ def load_unsafe_example():
325
+ return [4.5, 50.0, 45000.0, 11.0] # Really poor quality water
326
+
327
+ def load_borderline_example():
328
+ return [8.8, 320.0, 800.0, 4.5] # Borderline case with multiple issues
329
+
330
+ # Connect example buttons
331
+ safe_btn.click(load_safe_example, outputs=inputs)
332
+ unsafe_btn.click(load_unsafe_example, outputs=inputs)
333
+ neutral_btn.click(load_borderline_example, outputs=inputs)
334
+
335
+ # Footer
336
+ gr.Markdown("""
337
+ ---
338
+ ### ⚠️ Important Disclaimer
339
+
340
+ This tool provides **AI-based predictions for educational and research purposes only**.
341
+ For official water quality certification and regulatory compliance, always consult:
342
+ - Certified water testing laboratories
343
+ - Local health departments
344
+ - Environmental protection agencies
345
+
346
+ ### πŸ”¬ About This Model
347
+
348
+ - **Technology**: Machine Learning classification model
349
+ - **Parameters**: pH, Hardness, Total Dissolved Solids, Chloramines
350
+ - **Standards**: Based on WHO and EPA water quality guidelines
351
+ - **Purpose**: Educational demonstration of AI in water quality assessment
352
+
353
+ **Built with ❀️ for water quality research and education**
354
+ """)
355
 
356
+ return interface
357
+
358
+ # Create the interface
359
+ demo = create_interface()
360
 
361
+ # Launch the app
362
  if __name__ == "__main__":
363
+ demo.launch()