legend1234 commited on
Commit
b627819
·
1 Parent(s): c2d1168

Add initial files

Browse files
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ && rm -rf /var/lib/apt/lists/*
9
+
10
+ # Copy requirements first to leverage Docker cache
11
+ COPY requirements.txt .
12
+ RUN pip install --no-cache-dir -r requirements.txt
13
+
14
+ # Copy application code
15
+ COPY . .
16
+
17
+ # Create uploads directory
18
+ RUN mkdir -p uploads && chmod 777 uploads
19
+
20
+ # Create a non-root user
21
+ RUN useradd -m appuser && chown -R appuser:appuser /app
22
+ USER appuser
23
+
24
+ # # Set environment variables for the buffered output
25
+ # ENV PYTHONUNBUFFERED=1
26
+
27
+ # Default command (can be overridden in docker-compose.yml)
28
+ CMD ["gunicorn", "--config", "gunicorn_config.py", "app:app"]
Dockerfile.hf ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ # Install system dependencies
6
+ RUN apt-get update && apt-get install -y \
7
+ build-essential \
8
+ curl \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Copy requirements first to leverage Docker cache
12
+ COPY requirements.txt .
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ # Copy application code
16
+ COPY . .
17
+
18
+ # Create uploads directory
19
+ RUN mkdir -p uploads && chmod 777 uploads
20
+
21
+ # Create a non-root user
22
+ RUN useradd -m appuser && chown -R appuser:appuser /app
23
+ USER appuser
24
+
25
+ # Set environment variables
26
+ ENV FLASK_ENV=production
27
+ ENV REDIS_URL=redis://localhost:6379/0
28
+
29
+ # Expose ports
30
+ EXPOSE 7860
31
+ EXPOSE 6379
32
+ EXPOSE 5555
33
+
34
+ # Install and setup Redis
35
+ USER root
36
+ RUN apt-get update && apt-get install -y redis-server \
37
+ && rm -rf /var/lib/apt/lists/*
38
+
39
+ # Create start script
40
+ COPY <<EOF /app/start.sh
41
+ #!/bin/bash
42
+ # Start Redis
43
+ redis-server --daemonize yes
44
+
45
+ # Start Celery worker
46
+ celery -A app.celery worker --loglevel=info &
47
+
48
+ # Start Flower
49
+ celery -A app.celery flower --port=5555 &
50
+
51
+ # Start the Flask application with gunicorn
52
+ exec gunicorn --config gunicorn_config.py --bind 0.0.0.0:7860 app:app
53
+ EOF
54
+
55
+ RUN chmod +x /app/start.sh
56
+
57
+ # Switch back to non-root user
58
+ USER appuser
59
+
60
+ # Start all services
61
+ CMD ["/app/start.sh"]
README.md CHANGED
@@ -1,11 +1,53 @@
1
- ---
2
- title: Selector
3
- emoji: 🦀
4
- colorFrom: gray
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- license: gpl-3.0
9
- ---
10
-
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Selector Server
2
+
3
+ A web interface for the Selector library, providing tools for subset selection and diversity calculations.
4
+
5
+ ## Local Development
6
+
7
+ Run the server locally using Docker Compose:
8
+
9
+ ```bash
10
+ docker compose up --build --scale celery_worker=3 -d
11
+ ```
12
+
13
+ The server will be available at:
14
+ - Main application: http://localhost:8008
15
+ - Flower monitoring: http://localhost:5555
16
+
17
+ ## Hugging Face Deployment
18
+
19
+ 1. Build the Hugging Face specific Docker image:
20
+ ```bash
21
+ docker build -t selector-server-hf -f Dockerfile.hf .
22
+ ```
23
+
24
+ 2. Login to Hugging Face CLI:
25
+ ```bash
26
+ huggingface-cli login
27
+ ```
28
+
29
+ 3. Create a new Space on Hugging Face:
30
+ - Go to https://huggingface.co/spaces
31
+ - Click "Create new Space"
32
+ - Choose "Docker" as the Space SDK
33
+ - Set the hardware requirements (recommended: CPU + 16GB RAM)
34
+
35
+ 4. Push the Docker image:
36
+ ```bash
37
+ # Tag the image
38
+ docker tag selector-server-hf registry.hf.space/your-username/your-space-name:latest
39
+
40
+ # Push to Hugging Face
41
+ docker push registry.hf.space/your-username/your-space-name:latest
42
+ ```
43
+
44
+ The application will be available at: https://huggingface.co/spaces/your-username/your-space-name
45
+
46
+ ## Features
47
+
48
+ - Upload feature matrices for subset selection
49
+ - Calculate diversity metrics
50
+ - Visualize results
51
+ - RESTful API for programmatic access
52
+ - Celery-based task queue for long-running calculations
53
+ - Real-time task monitoring with Flower
app.py ADDED
@@ -0,0 +1,555 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # import json
2
+ import inspect
3
+ import io
4
+ import os
5
+ import shutil
6
+ import tempfile
7
+ import threading
8
+ import uuid
9
+ import warnings
10
+ from datetime import datetime
11
+ from typing import Callable, Dict
12
+
13
+ import markdown
14
+ import matplotlib.pyplot as plt
15
+ import numpy as np
16
+ import orjson
17
+ import pandas as pd
18
+ from flask import Flask, Response, render_template, request, send_file
19
+ from flask_status import FlaskStatus
20
+ from selector.methods.distance import DISE, MaxMin, MaxSum, OptiSim
21
+ from selector.methods.partition import GridPartition, Medoid
22
+ from selector.methods.similarity import NSimilarity
23
+ from selector.measures.diversity import compute_diversity
24
+ from sklearn.metrics import pairwise_distances
25
+ from werkzeug.utils import secure_filename
26
+
27
+ try:
28
+ from celery_config import celery
29
+
30
+ CELERY_AVAILABLE = True
31
+ except ImportError:
32
+ CELERY_AVAILABLE = False
33
+
34
+ app = Flask(__name__)
35
+ app_status = FlaskStatus(app)
36
+ app.config["MAX_CONTENT_LENGTH"] = 32 * 1024 * 1024 # 32MB max file size
37
+ app.config["UPLOAD_FOLDER"] = "uploads"
38
+ file_lock = threading.Lock()
39
+
40
+ # Ensure upload directory exists
41
+ os.makedirs(app.config["UPLOAD_FOLDER"], exist_ok=True)
42
+
43
+ ALLOWED_EXTENSIONS = {"txt", "npz", "xlsx", "xls"}
44
+
45
+ # Map algorithm names to their functions
46
+ SELECTION_ALGORITHM_MAP = {
47
+ # Distance-based methods
48
+ "MaxMin": MaxMin,
49
+ "MaxSum": MaxSum,
50
+ "OptiSim": OptiSim,
51
+ "DISE": DISE,
52
+ # Partition-based methods
53
+ "GridPartition": GridPartition,
54
+ # Similarity-based methods
55
+ "NSimilarity": NSimilarity,
56
+ }
57
+
58
+
59
+ def allowed_file(filename):
60
+ return "." in filename and filename.rsplit(".", 1)[1].lower() in ALLOWED_EXTENSIONS
61
+
62
+
63
+ def get_unique_upload_dir():
64
+ """Create a unique directory for each upload session."""
65
+ unique_dir = os.path.join(app.config["UPLOAD_FOLDER"], str(uuid.uuid4()))
66
+ os.makedirs(unique_dir, exist_ok=True)
67
+ return unique_dir
68
+
69
+
70
+ def clean_upload_dir(directory):
71
+ """Safely clean up upload directory."""
72
+ try:
73
+ if os.path.exists(directory):
74
+ shutil.rmtree(directory)
75
+ except Exception as e:
76
+ print(f"Error cleaning directory {directory}: {e}")
77
+
78
+
79
+ def load_data(filepath):
80
+ """Load data from various file formats."""
81
+ try:
82
+ ext = filepath.rsplit(".", 1)[1].lower()
83
+ if ext == "npz":
84
+ with np.load(filepath) as data:
85
+ return data["arr_0"] if "arr_0" in data else next(iter(data.values()))
86
+ elif ext == "txt":
87
+ return np.loadtxt(filepath)
88
+ elif ext in ["xlsx", "xls"]:
89
+ df = pd.read_excel(filepath)
90
+ return df.to_numpy()
91
+ except Exception as e:
92
+ raise ValueError(f"Error loading file {filepath}: {str(e)}")
93
+
94
+
95
+ def create_json_response(data, status=200):
96
+ """Create a JSON response using orjson for better numpy array handling"""
97
+ return Response(
98
+ orjson.dumps(data, option=orjson.OPT_SERIALIZE_NUMPY, default=str),
99
+ status=status,
100
+ mimetype="application/json",
101
+ )
102
+
103
+
104
+ def read_markdown_file(filename):
105
+ """Read and convert markdown file to HTML."""
106
+ filepath = os.path.join(os.path.dirname(__file__), "md_files", filename)
107
+ try:
108
+ with open(filepath, "r", encoding="utf-8") as f:
109
+ content = f.read()
110
+
111
+ # Pre-process math blocks to protect them
112
+ # content = content.replace('\\\\', '\\\\\\\\') # Escape backslashes in math
113
+
114
+ # Convert markdown to HTML with math and table support
115
+ md = markdown.Markdown(extensions=["tables", "fenced_code", "codehilite", "attr_list"])
116
+
117
+ # First pass: convert markdown to HTML
118
+ html = md.convert(content)
119
+
120
+ # Post-process math blocks
121
+ # Handle display math ($$...$$)
122
+ html = html.replace("<p>$$", '<div class="math-block">$$')
123
+ html = html.replace("$$</p>", "$$</div>")
124
+
125
+ # Handle inline math ($...$)
126
+ # We don't need special handling for inline math as MathJax will handle it
127
+
128
+ return html
129
+ except Exception as e:
130
+ print(f"Error reading markdown file {filename}: {e}")
131
+ return f"<p>Error loading content: {str(e)}</p>"
132
+
133
+
134
+ def get_default_parameters(func):
135
+ """Get default parameters for a function from its signature."""
136
+ sig = inspect.signature(func)
137
+ defaults = {}
138
+
139
+ for name, param in sig.parameters.items():
140
+ if name == "self" or name == "fun_dist": # Skip self and dist_metric
141
+ continue
142
+ if param.default is not param.empty:
143
+ defaults[name] = param.default
144
+
145
+ return defaults
146
+
147
+
148
+ @app.route("/get_default_params/<algorithm>")
149
+ def get_default_params(algorithm):
150
+ """API endpoint to get default parameters for an algorithm."""
151
+ if algorithm not in SELECTION_ALGORITHM_MAP:
152
+ return create_json_response({"error": f"Unknown algorithm: {algorithm}"}, 400)
153
+
154
+ try:
155
+ # Get the algorithm class
156
+ algorithm_class = SELECTION_ALGORITHM_MAP[algorithm]
157
+ # Get default parameters from __init__
158
+ params = get_default_parameters(algorithm_class.__init__)
159
+ return create_json_response(params)
160
+ except Exception as e:
161
+ return create_json_response({"error": f"Error getting parameters: {str(e)}"}, 500)
162
+
163
+
164
+ @app.route("/get_default_selection_params/<algorithm>")
165
+ def get_default_selection_params(algorithm):
166
+ """API endpoint to get default parameters for a selection algorithm."""
167
+ if algorithm not in SELECTION_ALGORITHM_MAP:
168
+ return create_json_response({"error": f"Algorithm unsupported: {algorithm}"}, 400)
169
+
170
+ try:
171
+ return create_json_response(get_default_selection_params(algorithm))
172
+ except Exception as e:
173
+ return create_json_response({"error": f"Error getting parameters: {str(e)}"}, 500)
174
+
175
+
176
+ @app.route("/")
177
+ def home():
178
+ return render_template("index.html")
179
+
180
+
181
+ @app.route("/md/<filename>")
182
+ def get_markdown(filename):
183
+ """Serve markdown files as HTML."""
184
+ if not filename.endswith(".md"):
185
+ filename = filename + ".md"
186
+ html = read_markdown_file(filename)
187
+ return create_json_response({"html": html})
188
+
189
+
190
+ def process_selection(arr, algorithm, parameters, dist_metric):
191
+ """
192
+ Process feature matrix using the specified selection algorithm.
193
+
194
+ Parameters
195
+ ----------
196
+ arr : np.ndarray
197
+ Input feature matrix
198
+ algorithm : str
199
+ Name of the selection algorithm to use
200
+ parameters : dict
201
+ Parameters for the algorithm
202
+ dist_metric : str, optional
203
+ Distance function to use.
204
+
205
+ Returns
206
+ -------
207
+ dict
208
+ Dictionary containing results and any warnings
209
+ """
210
+ result = {"success": False, "error": None, "warnings": [], "indices": None}
211
+
212
+ try:
213
+ # Get the algorithm class
214
+ algorithm_class = SELECTION_ALGORITHM_MAP.get(algorithm)
215
+
216
+ if algorithm_class is None:
217
+ raise ValueError(f"Unknown algorithm: {algorithm}")
218
+
219
+ # Get size parameter
220
+ size = parameters.pop('size', None)
221
+ if size is None:
222
+ raise ValueError("Subset size must be specified")
223
+
224
+ try:
225
+ size = int(size)
226
+ if size < 1:
227
+ raise ValueError
228
+ except (TypeError, ValueError):
229
+ raise ValueError("Subset size must be a positive integer")
230
+
231
+ # Validate size against array dimensions
232
+ if size > arr.shape[0]:
233
+ raise ValueError(f"Subset size ({size}) cannot be larger than the number of samples ({arr.shape[0]})")
234
+
235
+ # Handle distance-based methods differently
236
+ is_distance_based = algorithm in ["MaxMin", "MaxSum", "OptiSim", "DISE"]
237
+
238
+ # Convert array to float for computations
239
+ arr_float = arr.astype(float)
240
+
241
+ # Compute or prepare the input matrix
242
+ if is_distance_based:
243
+ # For distance-based methods, compute distance matrix
244
+ try:
245
+ if dist_metric and dist_metric != "":
246
+ # Use specified distance metric
247
+ arr_dist = pairwise_distances(arr_float, metric=dist_metric)
248
+ else:
249
+ # Default to euclidean distance
250
+ arr_dist = pairwise_distances(arr_float, metric='euclidean')
251
+ except Exception as e:
252
+ raise ValueError(f"Error computing distance matrix: {str(e)}")
253
+ else:
254
+ # For non-distance-based methods, use the original float array
255
+ arr_dist = arr_float
256
+
257
+ # Handle special case for GridPartition
258
+ if algorithm == "GridPartition":
259
+ # Ensure nbins_axis is provided and is an integer
260
+ nbins_axis = parameters.get('nbins_axis')
261
+ if nbins_axis is None:
262
+ raise ValueError("nbins_axis must be specified for GridPartition")
263
+ try:
264
+ parameters['nbins_axis'] = int(nbins_axis)
265
+ if parameters['nbins_axis'] < 1:
266
+ raise ValueError
267
+ except (TypeError, ValueError):
268
+ raise ValueError("nbins_axis must be a positive integer")
269
+
270
+ # Initialize and run the algorithm
271
+ try:
272
+ collector = algorithm_class(**parameters)
273
+ indices = collector.select(arr_dist, size=size)
274
+
275
+ # Ensure indices are valid
276
+ if indices is None:
277
+ raise ValueError("Algorithm returned None instead of indices")
278
+ if len(indices) != size:
279
+ warnings.warn(f"Algorithm returned {len(indices)} indices but expected {size}")
280
+
281
+ # Convert indices to list and validate
282
+ indices_list = indices.tolist() if isinstance(indices, np.ndarray) else list(indices)
283
+ if not all(isinstance(i, (int, np.integer)) and 0 <= i < arr.shape[0] for i in indices_list):
284
+ raise ValueError("Algorithm returned invalid indices")
285
+
286
+ result["success"] = True
287
+ result["indices"] = indices_list
288
+
289
+ except Exception as e:
290
+ import traceback
291
+ print(f"Traceback: {traceback.format_exc()}")
292
+ raise ValueError(f"Error executing algorithm: {str(e)}")
293
+
294
+ except Warning as w:
295
+ result["warnings"].append(str(w))
296
+ except Exception as e:
297
+ result["error"] = str(e)
298
+
299
+ return result
300
+
301
+
302
+ @app.route("/upload_selection", methods=["POST"])
303
+ def upload_selection_file():
304
+ """Handle file upload and process selection."""
305
+ try:
306
+ print("Debug - Starting upload_selection_file")
307
+
308
+ if "file" not in request.files:
309
+ return create_json_response({"error": "No file provided"}, 400)
310
+
311
+ file = request.files["file"]
312
+ if file.filename == "":
313
+ return create_json_response({"error": "No file selected"}, 400)
314
+
315
+ if not allowed_file(file.filename):
316
+ return create_json_response({"error": "File type not allowed"}, 400)
317
+
318
+ # Get parameters
319
+ algorithm = request.form.get("algorithm")
320
+ if not algorithm:
321
+ return create_json_response({"error": "No algorithm specified"}, 400)
322
+
323
+ # Get size parameter
324
+ size = request.form.get("size")
325
+ if not size:
326
+ return create_json_response({"error": "Subset size must be specified"}, 400)
327
+
328
+ # Get distance function
329
+ dist_metric = request.form.get("func_dist", "")
330
+
331
+ # Parse parameters
332
+ try:
333
+ parameters = orjson.loads(request.form.get("parameters", "{}"))
334
+ except Exception as e:
335
+ parameters = {}
336
+
337
+ # Add size to parameters
338
+ parameters["size"] = size
339
+
340
+ # Create a unique directory for this upload
341
+ upload_dir = get_unique_upload_dir()
342
+
343
+ try:
344
+ # Save file with unique name
345
+ file_path = os.path.join(
346
+ upload_dir, secure_filename(str(uuid.uuid4()) + "_" + file.filename)
347
+ )
348
+
349
+ with file_lock:
350
+ file.save(file_path)
351
+
352
+ # Load data
353
+ array = load_data(file_path)
354
+
355
+ # Process the selection with separate dist_metric parameter
356
+ result = process_selection(array, algorithm, parameters, dist_metric)
357
+
358
+ return create_json_response(result)
359
+
360
+ except Exception as e:
361
+ return create_json_response({"error": str(e)}, 500)
362
+
363
+ finally:
364
+ # Clean up the unique upload directory
365
+ clean_upload_dir(upload_dir)
366
+
367
+ except Exception as e:
368
+ return create_json_response({"error": f"Error processing request: {str(e)}"}, 400)
369
+
370
+
371
+ @app.route("/download", methods=["POST"])
372
+ def download():
373
+ """Download selected indices in specified format."""
374
+ try:
375
+ data = request.get_json()
376
+ if not data or "indices" not in data:
377
+ return create_json_response({"error": "No indices provided"}, 400)
378
+
379
+ indices = data["indices"]
380
+ format = data.get("format", "txt")
381
+ timestamp = data.get("timestamp", datetime.now().strftime("%Y%m%d-%H%M%S"))
382
+
383
+ # Create a BytesIO buffer for the file
384
+ buffer = io.BytesIO()
385
+
386
+ # Define format-specific settings
387
+ format_settings = {
388
+ "txt": {
389
+ "extension": "txt",
390
+ "mimetype": "text/plain",
391
+ "processor": lambda b, d: b.write("\n".join(map(str, d)).encode()),
392
+ },
393
+ "npz": {
394
+ "extension": "npz",
395
+ "mimetype": "application/octet-stream",
396
+ "processor": lambda b, d: np.savez_compressed(b, indices=np.array(d)),
397
+ },
398
+ "xlsx": {
399
+ "extension": "xlsx",
400
+ "mimetype": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
401
+ "processor": lambda b, d: pd.DataFrame({"selected_indices": d}).to_excel(
402
+ b, index=False
403
+ ),
404
+ },
405
+ }
406
+
407
+ if format not in format_settings:
408
+ return create_json_response({"error": f"Unsupported format: {format}"}, 400)
409
+
410
+ settings = format_settings[format]
411
+
412
+ # Process the file
413
+ settings["processor"](buffer, indices)
414
+
415
+ # Create filename with correct extension
416
+ filename = f'selected_indices_{timestamp}.{settings["extension"]}'
417
+
418
+ # Seek to beginning of file
419
+ buffer.seek(0)
420
+
421
+ return send_file(
422
+ buffer, mimetype=settings["mimetype"], as_attachment=True, download_name=filename
423
+ )
424
+
425
+ except Exception as e:
426
+ print(f"Error in download: {str(e)}")
427
+ return create_json_response({"error": str(e)}, 500)
428
+
429
+
430
+ @app.route("/calculate_diversity", methods=["POST"])
431
+ def calculate_diversity():
432
+ """Calculate diversity score for the given feature subset."""
433
+ try:
434
+ # Get files from request
435
+ feature_subset_file = request.files.get('feature_subset')
436
+ features_file = request.files.get('features')
437
+
438
+ if not feature_subset_file:
439
+ return create_json_response({"error": "Feature subset file is required"}, 400)
440
+
441
+ # Get other parameters
442
+ div_type = request.form.get('div_type', 'shannon_entropy')
443
+ div_parameters = orjson.loads(request.form.get('div_parameters', '{}'))
444
+
445
+ # Read feature subset
446
+ try:
447
+ # Save the uploaded file
448
+ feature_subset_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(feature_subset_file.filename))
449
+ feature_subset_file.save(feature_subset_path)
450
+
451
+ # Read the feature subset file
452
+ feature_subset = load_data(feature_subset_path)
453
+ if feature_subset is None:
454
+ raise ValueError(f"Failed to read feature subset file: {feature_subset_file.filename}")
455
+
456
+ # Convert to float array
457
+ feature_subset = feature_subset.astype(float)
458
+
459
+ # Clean up the temporary file
460
+ os.remove(feature_subset_path)
461
+ except Exception as e:
462
+ return create_json_response({"error": f"Error reading feature subset file: {str(e)}"}, 400)
463
+
464
+ # Read features if provided
465
+ features = None
466
+ if features_file:
467
+ try:
468
+ # Save the uploaded file
469
+ features_path = os.path.join(app.config['UPLOAD_FOLDER'], secure_filename(features_file.filename))
470
+ features_file.save(features_path)
471
+
472
+ # Read the features file
473
+ features = load_data(features_path)
474
+ if features is None:
475
+ raise ValueError(f"Failed to read features file: {features_file.filename}")
476
+
477
+ # Convert to float array
478
+ features = features.astype(float)
479
+
480
+ # Clean up the temporary file
481
+ os.remove(features_path)
482
+ except Exception as e:
483
+ return create_json_response({"error": f"Error reading features file: {str(e)}"}, 400)
484
+
485
+ # Extract parameters
486
+ normalize = div_parameters.get('normalize', False)
487
+ truncation = div_parameters.get('truncation', False)
488
+ cs = div_parameters.get('cs', None)
489
+
490
+ # Calculate diversity
491
+ try:
492
+ diversity_score = compute_diversity(
493
+ feature_subset=feature_subset,
494
+ div_type=div_type,
495
+ normalize=normalize,
496
+ truncation=truncation,
497
+ features=features,
498
+ cs=cs
499
+ )
500
+
501
+ return create_json_response({
502
+ "success": True,
503
+ "diversity_score": float(diversity_score)
504
+ })
505
+
506
+ except Exception as e:
507
+ import traceback
508
+ print(f"Error calculating diversity: {str(e)}")
509
+ print(f"Traceback: {traceback.format_exc()}")
510
+ return create_json_response({"error": f"Error calculating diversity: {str(e)}"}, 400)
511
+
512
+ except Exception as e:
513
+ return create_json_response({"error": str(e)}, 500)
514
+
515
+
516
+ @app.route("/status")
517
+ def server_status():
518
+ """Return server status"""
519
+ status = {
520
+ "status": "ok",
521
+ "message": "Server is running",
522
+ "timestamp": datetime.now().isoformat(),
523
+ "components": {"flask": True, "celery": False, "redis": False},
524
+ }
525
+
526
+ if CELERY_AVAILABLE:
527
+ # Check Celery
528
+ try:
529
+ celery.control.ping(timeout=1)
530
+ status["components"]["celery"] = True
531
+ except Exception as e:
532
+ print(f"Celery check failed: {e}")
533
+
534
+ # Check Redis
535
+ try:
536
+ redis_client = celery.backend.client
537
+ redis_client.ping()
538
+ status["components"]["redis"] = True
539
+ except Exception as e:
540
+ print(f"Redis check failed: {e}")
541
+
542
+ # Set overall status
543
+ if not all(status["components"].values()):
544
+ status["status"] = "degraded"
545
+ status["message"] = "Some components are not available"
546
+ else:
547
+ status["message"] = "Running without Celery/Redis support"
548
+
549
+ return create_json_response(status)
550
+
551
+
552
+ if __name__ == "__main__":
553
+ app.run(debug=True, host="0.0.0.0", port=8008)
554
+ from flask_debugtoolbar import DebugToolbarExtension
555
+ toolbar = DebugToolbarExtension(app)
celery_config.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from celery import Celery
2
+
3
+ celery = Celery("procrustes_server", broker="redis://redis:6379/0", backend="redis://redis:6379/0")
4
+
5
+ celery.conf.update(
6
+ worker_max_tasks_per_child=1000,
7
+ worker_prefetch_multiplier=1,
8
+ task_acks_late=True,
9
+ task_reject_on_worker_lost=True,
10
+ broker_pool_limit=None,
11
+ broker_connection_timeout=30,
12
+ result_expires=3600, # Results expire after 1 hour
13
+ task_track_started=True,
14
+ task_time_limit=300, # 5 minutes
15
+ task_soft_time_limit=240, # 4 minutes
16
+ worker_concurrency=4, # Number of worker processes per Celery worker
17
+ )
docker-compose.yml ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ web:
5
+ build: .
6
+ command: gunicorn --config gunicorn_config.py app:app --reload
7
+ expose:
8
+ - "8008"
9
+ volumes:
10
+ - .:/app
11
+ - upload_data:/app/uploads
12
+ depends_on:
13
+ - redis
14
+ environment:
15
+ - FLASK_ENV=development
16
+ - REDIS_URL=redis://redis:6379/0
17
+ deploy:
18
+ replicas: 2
19
+ resources:
20
+ limits:
21
+ cpus: '1'
22
+ memory: 2G
23
+ restart: unless-stopped
24
+
25
+ redis:
26
+ image: redis:7-alpine
27
+ command: redis-server --appendonly yes
28
+ volumes:
29
+ - redis_data:/data
30
+ ports:
31
+ - "6379:6379"
32
+ deploy:
33
+ resources:
34
+ limits:
35
+ cpus: '0.5'
36
+ memory: 1G
37
+ restart: unless-stopped
38
+
39
+ celery_worker:
40
+ build: .
41
+ command: celery -A app.celery worker --loglevel=info
42
+ volumes:
43
+ - .:/app
44
+ - upload_data:/app/uploads
45
+ depends_on:
46
+ - redis
47
+ environment:
48
+ - REDIS_URL=redis://redis:6379/0
49
+ deploy:
50
+ replicas: 4
51
+ resources:
52
+ limits:
53
+ cpus: '1'
54
+ memory: 2G
55
+ restart: unless-stopped
56
+
57
+ celery_flower:
58
+ build: .
59
+ command: celery -A app.celery flower
60
+ ports:
61
+ - "5555:5555"
62
+ volumes:
63
+ - .:/app
64
+ - flower_data:/app/flower
65
+ depends_on:
66
+ - redis
67
+ - celery_worker
68
+ environment:
69
+ - REDIS_URL=redis://redis:6379/0
70
+ deploy:
71
+ resources:
72
+ limits:
73
+ cpus: '0.5'
74
+ memory: 512M
75
+ restart: unless-stopped
76
+
77
+ nginx:
78
+ image: nginx:alpine
79
+ ports:
80
+ - "8008:8008"
81
+ volumes:
82
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
83
+ - .:/app:ro
84
+ depends_on:
85
+ - web
86
+ deploy:
87
+ resources:
88
+ limits:
89
+ cpus: '0.5'
90
+ memory: 512M
91
+ restart: unless-stopped
92
+
93
+ volumes:
94
+ redis_data:
95
+ upload_data:
96
+ flower_data:
gunicorn_config.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import multiprocessing
2
+ import os
3
+
4
+ # Number of worker processes - adjust based on CPU cores
5
+ workers = multiprocessing.cpu_count() * 2 + 1
6
+
7
+ # Number of threads per worker
8
+ threads = 4
9
+
10
+ # Maximum number of pending connections
11
+ backlog = 2048
12
+
13
+ # Maximum number of requests a worker will process before restarting
14
+ max_requests = 10000
15
+ max_requests_jitter = 50
16
+
17
+ # Timeout for worker processes (5 minutes)
18
+ timeout = 300
19
+
20
+ # Keep-alive timeout
21
+ keepalive = 5
22
+
23
+ # Log settings
24
+ loglevel = "info"
25
+ accesslog = "-"
26
+ errorlog = "-"
27
+ access_log_format = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
28
+
29
+ # Bind address - use HF_PORT for Hugging Face deployment
30
+ port = os.getenv('PORT', '8008') # HF uses PORT env var
31
+ bind = f"0.0.0.0:{port}"
32
+
33
+ # Worker class
34
+ worker_class = "gevent"
35
+
36
+ # Process name
37
+ proc_name = "selector_server"
38
+
39
+ # Preload app for faster worker spawning
40
+ preload_app = True
41
+
42
+ # Graceful timeout
43
+ graceful_timeout = 30
44
+
45
+ # Server mechanics
46
+ daemon = False
47
+ pidfile = None
48
+ umask = 0
49
+ user = None
50
+ group = None
51
+ tmp_upload_dir = None
52
+
53
+ # Logging
54
+ capture_output = True
55
+ enable_stdio_inheritance = False
md_files/about.md ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Motivation
2
+
3
+ Selecting diverse and representative subsets is crucial for the data-driven models and machine learning applications in many science and engineering disciplines, especially for molecular design and drug discovery. Motivated by this, we develop the Selector package, a free and open-source Python library for selecting diverse subsets.
4
+
5
+ The `Selector` library implements a range of existing algorithms for subset sampling based on the distance between and similarity of samples, as well as tools based on spatial partitioning. In addition, it includes seven diversity measures for quantifying the diversity of a given set. We also implemented various mathematical formulations to convert similarities into dissimilarities.
6
+
7
+ ## `Selector` Library
8
+
9
+ Selector is a free, open-source, and cross-platform Python library designed to help you effortlessly identify the most diverse subset of molecules from your dataset. Please use the following citation in any publication using Selector library:
10
+
11
+ ## Citation
12
+ Please use the following citation in any publication using the `Selector` library:
13
+
14
+ **To be added**
15
+
16
+ ## More Information
17
+
18
+ For more information about the Selector library, please visit our [GitHub repository](https://github.com/qcdevs/selector) and documentation at [https://selector.qcdevs.org](https://selector.qcdevs.org).
19
+
20
+ ## Acknowledgments
21
+
22
+ This webserver is supported by the DRI EDIA Champions Pilot Program. We are grateful to the [Digital Research Alliance](https://alliancecan.ca/) for providing the computing resources.
md_files/contacts.md ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ The `Selector` source code is hosted on [GitHub](https://github.com/theochem/Selector) and is released under the [GNU General Public License v3.0](https://github.com/theochem/Selector/blob/master/LICENSE). We welcome any contributions to the Selector library in accordance with our Code of Conduct; please see our [Contributing Guidelines](https://qcdevs.org/guidelines/QCDevsCodeOfConduct/). Please report any issues you encounter while using Selector library on [GitHub Issues](https://github.com/theochem/Selector/issues). For further information and inquiries, please contact us at [[email protected]](mailto:[email protected]).
2
+
nginx.conf ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ user nginx;
2
+ worker_processes auto;
3
+ error_log /var/log/nginx/error.log warn;
4
+ pid /var/run/nginx.pid;
5
+
6
+ events {
7
+ worker_connections 4096;
8
+ use epoll;
9
+ multi_accept on;
10
+ }
11
+
12
+ http {
13
+ include /etc/nginx/mime.types;
14
+ default_type application/octet-stream;
15
+
16
+ log_format main '$remote_addr - $remote_user [$time_local] "$request" '
17
+ '$status $body_bytes_sent "$http_referer" '
18
+ '"$http_user_agent" "$http_x_forwarded_for"';
19
+
20
+ access_log /var/log/nginx/access.log main;
21
+
22
+ sendfile on;
23
+ tcp_nopush on;
24
+ tcp_nodelay on;
25
+ keepalive_timeout 65;
26
+ types_hash_max_size 2048;
27
+ client_max_body_size 32M;
28
+
29
+ # Gzip compression
30
+ gzip on;
31
+ gzip_vary on;
32
+ gzip_proxied any;
33
+ gzip_comp_level 6;
34
+ gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml;
35
+
36
+ upstream selector_backend {
37
+ server web:8008;
38
+ }
39
+
40
+ server {
41
+ listen 8008;
42
+ server_name localhost;
43
+
44
+ location / {
45
+ proxy_pass http://selector_backend;
46
+ proxy_set_header Host $host;
47
+ proxy_set_header X-Real-IP $remote_addr;
48
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
49
+ proxy_set_header X-Forwarded-Proto $scheme;
50
+ }
51
+
52
+ location /flower/ {
53
+ rewrite ^/flower/(.*)$ /$1 break;
54
+ proxy_pass http://celery_flower:5555;
55
+ proxy_set_header Host $host;
56
+ proxy_redirect off;
57
+ proxy_http_version 1.1;
58
+ proxy_set_header Upgrade $http_upgrade;
59
+ proxy_set_header Connection "upgrade";
60
+ }
61
+ }
62
+ }
prometheus.yml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ global:
2
+ scrape_interval: 15s
3
+ evaluation_interval: 15s
4
+
5
+ scrape_configs:
6
+ - job_name: 'prometheus'
7
+ static_configs:
8
+ - targets: ['localhost:9090']
9
+
10
+ - job_name: 'flask'
11
+ static_configs:
12
+ - targets: ['web:8000']
13
+
14
+ - job_name: 'redis'
15
+ static_configs:
16
+ - targets: ['redis-exporter:9121']
17
+
18
+ - job_name: 'flower'
19
+ static_configs:
20
+ - targets: ['flower:5555']
requirements.txt ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ flask>=3.0.0
2
+ qc-selector>=0.0.3
3
+ numpy>=2.0.0
4
+ pandas>=2.1.4
5
+ openpyxl>=3.0.9
6
+ werkzeug>=3.0.0
7
+ gunicorn>=22.0.0
8
+ gevent>=23.9.1
9
+ redis>=5.0.1
10
+ celery>=5.3.6
11
+ flower>=2.0.1
12
+ orjson>=3.10.12
13
+ flask_status>=1.0.1
14
+ markdown>=3.5.1
15
+ Pygments>=2.17.2
16
+ matplotlib>=3.8.0
17
+ scikit-learn>=1.5.0
templates/index.html ADDED
@@ -0,0 +1,998 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Selector Web Server</title>
7
+ <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet">
8
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --sidebar-width: 280px;
13
+ --primary-color: #2563eb;
14
+ --sidebar-bg: #f8fafc;
15
+ --header-bg: #f1f5f9;
16
+ --border-color: #e2e8f0;
17
+ --text-primary: #1e293b;
18
+ --text-secondary: #64748b;
19
+ }
20
+
21
+ body {
22
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
23
+ color: var(--text-primary);
24
+ line-height: 1.6;
25
+ }
26
+
27
+ .wrapper {
28
+ display: flex;
29
+ width: 100%;
30
+ align-items: stretch;
31
+ }
32
+
33
+ #sidebar {
34
+ min-width: var(--sidebar-width);
35
+ max-width: var(--sidebar-width);
36
+ min-height: 100vh;
37
+ transition: all 0.3s ease-out;
38
+ background-color: var(--sidebar-bg);
39
+ border-right: 1px solid var(--border-color);
40
+ }
41
+
42
+ #sidebar.active {
43
+ margin-left: calc(-1 * var(--sidebar-width));
44
+ }
45
+
46
+ #content {
47
+ width: 100%;
48
+ padding: 2rem;
49
+ min-height: 100vh;
50
+ margin-left: 0;
51
+ transition: all 0.3s ease-out;
52
+ background-color: white;
53
+ padding-bottom: 4rem;
54
+ }
55
+
56
+ .sidebar-header {
57
+ padding: 1.5rem;
58
+ background: var(--header-bg);
59
+ display: flex;
60
+ align-items: center;
61
+ justify-content: space-between;
62
+ border-bottom: 1px solid var(--border-color);
63
+ }
64
+
65
+ #sidebarCollapse, #showSidebarBtn {
66
+ background: transparent;
67
+ border: none;
68
+ padding: 0.5rem;
69
+ color: var(--text-primary);
70
+ border-radius: 0.375rem;
71
+ display: flex;
72
+ align-items: center;
73
+ justify-content: center;
74
+ cursor: pointer;
75
+ transition: all 0.2s;
76
+ }
77
+
78
+ #sidebarCollapse:hover, #showSidebarBtn:hover {
79
+ background: rgba(0, 0, 0, 0.05);
80
+ transform: translateX(2px);
81
+ }
82
+
83
+ #sidebarCollapse i {
84
+ transition: transform 0.3s ease;
85
+ }
86
+
87
+ #sidebar.active #sidebarCollapse i {
88
+ transform: rotate(180deg);
89
+ }
90
+
91
+ #showSidebarBtn {
92
+ position: fixed;
93
+ left: 1.5rem;
94
+ top: 1.5rem;
95
+ z-index: 1000;
96
+ display: none;
97
+ background-color: white;
98
+ border: 1px solid var(--border-color);
99
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
100
+ }
101
+
102
+ #showSidebarBtn.show {
103
+ display: flex;
104
+ }
105
+
106
+ .sidebar-title {
107
+ margin: 0;
108
+ font-weight: 600;
109
+ color: var(--text-primary);
110
+ font-size: 1.125rem;
111
+ }
112
+
113
+ .components {
114
+ padding: 1.5rem 0;
115
+ }
116
+
117
+ .components a {
118
+ padding: 0.75rem 1.5rem;
119
+ display: flex;
120
+ align-items: center;
121
+ color: var(--text-secondary);
122
+ text-decoration: none;
123
+ transition: all 0.2s;
124
+ font-weight: 500;
125
+ }
126
+
127
+ .components a i {
128
+ margin-right: 0.75rem;
129
+ font-size: 1.25rem;
130
+ }
131
+
132
+ .components a:hover {
133
+ background: var(--header-bg);
134
+ color: var(--text-primary);
135
+ }
136
+
137
+ .components a.active {
138
+ background: var(--header-bg);
139
+ color: var(--primary-color);
140
+ border-right: 3px solid var(--primary-color);
141
+ }
142
+
143
+ .card {
144
+ border: 1px solid var(--border-color);
145
+ border-radius: 0.5rem;
146
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
147
+ }
148
+
149
+ .card-body {
150
+ padding: 1.5rem;
151
+ }
152
+
153
+ .form-label {
154
+ font-weight: 500;
155
+ color: var(--text-primary);
156
+ margin-bottom: 0.5rem;
157
+ }
158
+
159
+ .form-control, .form-select {
160
+ border-color: var(--border-color);
161
+ border-radius: 0.375rem;
162
+ padding: 0.625rem;
163
+ }
164
+
165
+ .form-control:focus, .form-select:focus {
166
+ border-color: var(--primary-color);
167
+ box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.1);
168
+ }
169
+
170
+ .btn-primary {
171
+ background-color: var(--primary-color);
172
+ border: none;
173
+ padding: 0.625rem 1.25rem;
174
+ font-weight: 500;
175
+ }
176
+
177
+ .btn-primary:hover {
178
+ background-color: #1d4ed8;
179
+ }
180
+
181
+ .btn-success, .btn-info {
182
+ font-weight: 500;
183
+ padding: 0.625rem 1.25rem;
184
+ }
185
+
186
+ .loading {
187
+ display: none;
188
+ align-items: center;
189
+ gap: 0.75rem;
190
+ padding: 1rem;
191
+ background-color: var(--sidebar-bg);
192
+ border-radius: 0.375rem;
193
+ }
194
+
195
+ .result-section {
196
+ display: none;
197
+ margin-top: 2rem;
198
+ }
199
+
200
+ .bg-light {
201
+ background-color: var(--sidebar-bg) !important;
202
+ border-radius: 0.375rem;
203
+ }
204
+
205
+ h2 {
206
+ font-weight: 600;
207
+ margin-bottom: 1.5rem;
208
+ color: var(--text-primary);
209
+ }
210
+
211
+ h5, h6 {
212
+ font-weight: 600;
213
+ color: var(--text-primary);
214
+ }
215
+
216
+ .footer {
217
+ position: fixed;
218
+ bottom: 0;
219
+ width: 100%;
220
+ background-color: var(--header-bg);
221
+ border-top: 1px solid var(--border-color);
222
+ padding: 1rem;
223
+ display: flex;
224
+ justify-content: space-between;
225
+ align-items: center;
226
+ z-index: 1000;
227
+ }
228
+
229
+ .footer .copyright {
230
+ color: var(--text-secondary);
231
+ font-size: 0.875rem;
232
+ }
233
+
234
+ .footer .status {
235
+ display: flex;
236
+ align-items: center;
237
+ gap: 0.75rem;
238
+ color: var(--text-secondary);
239
+ font-size: 0.875rem;
240
+ }
241
+
242
+ .footer .status .status-indicator {
243
+ width: 8px;
244
+ height: 8px;
245
+ border-radius: 50%;
246
+ }
247
+
248
+ .footer .status .status-details {
249
+ display: none;
250
+ position: absolute;
251
+ bottom: 100%;
252
+ right: 1rem;
253
+ background: white;
254
+ border: 1px solid var(--border-color);
255
+ border-radius: 0.5rem;
256
+ padding: 1rem;
257
+ box-shadow: 0 -4px 6px -1px rgba(0, 0, 0, 0.1);
258
+ min-width: 200px;
259
+ }
260
+
261
+ .footer .status:hover .status-details {
262
+ display: block;
263
+ }
264
+
265
+ .status-details div {
266
+ margin-bottom: 0.5rem;
267
+ }
268
+
269
+ .status-details .component {
270
+ display: flex;
271
+ align-items: center;
272
+ gap: 0.5rem;
273
+ }
274
+
275
+ .status-details .component-indicator {
276
+ width: 6px;
277
+ height: 6px;
278
+ border-radius: 50%;
279
+ }
280
+
281
+ .status-details .resources {
282
+ margin-top: 0.75rem;
283
+ padding-top: 0.75rem;
284
+ border-top: 1px solid var(--border-color);
285
+ }
286
+
287
+ /* Markdown content styling */
288
+ .section {
289
+ padding: 2rem;
290
+ }
291
+
292
+ .section table {
293
+ border-collapse: collapse;
294
+ margin: 1rem 0;
295
+ width: 100%;
296
+ }
297
+
298
+ .section table th,
299
+ .section table td {
300
+ border: 1px solid var(--border-color);
301
+ padding: 0.75rem;
302
+ text-align: left;
303
+ }
304
+
305
+ .section table th {
306
+ background-color: var(--header-bg);
307
+ font-weight: 600;
308
+ }
309
+
310
+ .section table tr:nth-child(even) {
311
+ background-color: var(--header-bg);
312
+ }
313
+
314
+ .section h1,
315
+ .section h2,
316
+ .section h3 {
317
+ color: var(--text-primary);
318
+ margin-top: 2rem;
319
+ margin-bottom: 1rem;
320
+ }
321
+
322
+ .section h1 {
323
+ font-size: 2rem;
324
+ border-bottom: 2px solid var(--border-color);
325
+ padding-bottom: 0.5rem;
326
+ }
327
+
328
+ .section h2 {
329
+ font-size: 1.5rem;
330
+ }
331
+
332
+ .section h3 {
333
+ font-size: 1.25rem;
334
+ }
335
+
336
+ .section p {
337
+ margin-bottom: 1rem;
338
+ line-height: 1.6;
339
+ }
340
+
341
+ .section ul,
342
+ .section ol {
343
+ margin-bottom: 1rem;
344
+ padding-left: 2rem;
345
+ }
346
+
347
+ .section li {
348
+ margin-bottom: 0.5rem;
349
+ }
350
+
351
+ .section code {
352
+ background-color: var(--header-bg);
353
+ padding: 0.2rem 0.4rem;
354
+ border-radius: 0.25rem;
355
+ font-family: monospace;
356
+ }
357
+
358
+ .section pre {
359
+ background-color: var(--header-bg);
360
+ padding: 1rem;
361
+ border-radius: 0.5rem;
362
+ overflow-x: auto;
363
+ margin: 1rem 0;
364
+ }
365
+
366
+ .section a {
367
+ color: var(--primary-color);
368
+ text-decoration: none;
369
+ }
370
+
371
+ .section a:hover {
372
+ text-decoration: underline;
373
+ }
374
+
375
+ /* Math equations */
376
+ .section .MathJax_Display {
377
+ overflow-x: auto;
378
+ max-width: 100%;
379
+ margin: 1em 0;
380
+ }
381
+
382
+ .section .MathJax {
383
+ font-size: 1.1em !important;
384
+ }
385
+
386
+ .section .math-block {
387
+ overflow-x: auto;
388
+ margin: 1em 0;
389
+ text-align: center;
390
+ }
391
+ </style>
392
+ <!-- MathJax Configuration -->
393
+ <script type="text/x-mathjax-config">
394
+ MathJax.Hub.Config({
395
+ tex2jax: {
396
+ inlineMath: [['$','$'], ['\\(','\\)']],
397
+ displayMath: [['$$','$$'], ['\\[','\\]']],
398
+ processEscapes: true,
399
+ processEnvironments: true
400
+ },
401
+ displayAlign: 'center',
402
+ "HTML-CSS": {
403
+ styles: {'.MathJax_Display': {"margin": 0}},
404
+ linebreaks: { automatic: true }
405
+ }
406
+ });
407
+ </script>
408
+
409
+ <!-- MathJax Loading -->
410
+ <script type="text/javascript" src="https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"></script>
411
+ </head>
412
+ <body>
413
+ <button type="button" id="showSidebarBtn" class="btn">
414
+ <i class="bi bi-chevron-right"></i>
415
+ </button>
416
+
417
+ <div class="wrapper">
418
+ <!-- Sidebar -->
419
+ <nav id="sidebar">
420
+ <div class="sidebar-header">
421
+ <h5 class="sidebar-title">Selector Server</h5>
422
+ <button type="button" id="sidebarCollapse" class="btn">
423
+ <i class="bi bi-chevron-left"></i>
424
+ </button>
425
+ </div>
426
+ <ul class="nav flex-column">
427
+ <li class="nav-item">
428
+ <a href="#" class="nav-link active" data-section="selection">
429
+ <i class="bi bi-grid-3x3-gap me-2"></i>Subset Selection
430
+ </a>
431
+ </li>
432
+ <li class="nav-item">
433
+ <a href="#" class="nav-link" data-section="diversity">
434
+ <i class="bi bi-graph-up me-2"></i>Diversity Calculation
435
+ </a>
436
+ </li>
437
+ <li class="nav-item">
438
+ <a href="#" class="nav-link" data-section="about">
439
+ <i class="bi bi-info-circle me-2"></i>About
440
+ </a>
441
+ </li>
442
+ <li class="nav-item">
443
+ <a href="#" class="nav-link" data-section="contact">
444
+ <i class="bi bi-envelope me-2"></i>Contact
445
+ </a>
446
+ </li>
447
+ </ul>
448
+ </nav>
449
+
450
+ <!-- Page Content -->
451
+ <div id="content">
452
+ <!-- Selection Section -->
453
+ <div id="selection" class="section">
454
+ <h2>Subset Selection</h2>
455
+ <div class="card">
456
+ <div class="card-body">
457
+ <form id="selectionForm" enctype="multipart/form-data">
458
+ <div class="mb-3">
459
+ <label for="file" class="form-label">Upload Feature Matrix</label>
460
+ <input type="file" class="form-control" id="file" name="file" accept=".txt,.npz,.xlsx,.xls">
461
+ <small class="text-muted">Supported formats: .txt, .npz, .xlsx, .xls (max 32MB)</small>
462
+ </div>
463
+
464
+ <div class="mb-3">
465
+ <label for="size" class="form-label">Subset Size</label>
466
+ <input type="number" class="form-control" id="size" name="size" min="1" step="1" required>
467
+ <small class="text-muted">Number of data points to select (integer)</small>
468
+ </div>
469
+
470
+ <div class="mb-3">
471
+ <label for="distance_metric" class="form-label">Distance Function</label>
472
+ <select class="form-select" id="distance_metric" name="distance_metric">
473
+ <option value="euclidean">euclidean</option>
474
+ <option value="cityblock">cityblock</option>
475
+ <option value="cosine">cosine</option>
476
+ <option value="l1">l1</option>
477
+ <option value="l2">l2</option>
478
+ <option value="manhattan">manhattan</option>
479
+ <option value="braycurtis">braycurtis</option>
480
+ <option value="canberra">canberra</option>
481
+ <option value="chebyshev">chebyshev</option>
482
+ <option value="correlation">correlation</option>
483
+ <option value="dice">dice</option>
484
+ <option value="hamming">hamming</option>
485
+ <option value="jaccard">jaccard</option>
486
+ <option value="kulsinski">kulsinski</option>
487
+ <option value="mahalanobis">mahalanobis</option>
488
+ <option value="minkowski">minkowski</option>
489
+ <option value="rogerstanimoto">rogerstanimoto</option>
490
+ <option value="russellrao">russellrao</option>
491
+ <option value="seuclidean">seuclidean</option>
492
+ <option value="sokalmichener">sokalmichener</option>
493
+ <option value="sokalsneath">sokalsneath</option>
494
+ <option value="sqeuclidean">sqeuclidean</option>
495
+ <option value="yule">yule</option>
496
+ </select>
497
+ <small class="text-muted">Distance metric from scikit-learn's pairwise_distances (optional)</small>
498
+ </div>
499
+
500
+ <div class="mb-3">
501
+ <label for="algorithm" class="form-label">Selection Method</label>
502
+ <select class="form-select" id="algorithm" name="algorithm">
503
+ <optgroup label="Distance-based Methods">
504
+ <option value="MaxMin">MaxMin</option>
505
+ <option value="MaxSum">MaxSum</option>
506
+ <option value="OptiSim">OptiSim</option>
507
+ <option value="DISE">DISE</option>
508
+ </optgroup>
509
+ <optgroup label="Partition-based Methods">
510
+ <option value="GridPartition">Grid Partition</option>
511
+ </optgroup>
512
+ <optgroup label="Similarity-based Methods">
513
+ <option value="NSimilarity">NSimilarity</option>
514
+ </optgroup>
515
+ </select>
516
+ </div>
517
+
518
+ <div class="mb-3" id="nbins_axis_container" style="display: none;">
519
+ <label for="nbins_axis" class="form-label">Number of Bins per Axis</label>
520
+ <input type="number" class="form-control" id="nbins_axis" name="nbins_axis" min="1" value="5">
521
+ <small class="form-text text-muted">Number of bins to partition each axis into</small>
522
+ </div>
523
+
524
+ <div class="mb-3">
525
+ <label for="parameters" class="form-label">Additional Parameters (JSON format)</label>
526
+ <textarea class="form-control font-monospace" id="parameters" name="parameters" rows="10"></textarea>
527
+ </div>
528
+
529
+ <div class="d-grid gap-2">
530
+ <button type="submit" class="btn btn-primary">
531
+ <i class="bi bi-play-fill me-2"></i>Run Subset Selection
532
+ </button>
533
+ </div>
534
+ </form>
535
+ </div>
536
+ </div>
537
+
538
+ <div id="resultSection" class="mt-4" style="display: none;">
539
+ <div class="card">
540
+ <div class="card-body">
541
+ <h5 class="card-title">Results</h5>
542
+ <div id="warnings" class="alert alert-warning" style="display: none;"></div>
543
+ <div id="error" class="alert alert-danger" style="display: none;"></div>
544
+
545
+ <div id="success-content" style="display: none;">
546
+ <h4>Selected Indices:</h4>
547
+ <pre id="indices"></pre>
548
+ <div class="mb-3">
549
+ <label for="download-format" class="form-label">Download Format</label>
550
+ <select class="form-select" id="download-format">
551
+ <option value="txt">TXT</option>
552
+ <option value="npz">NPZ</option>
553
+ <option value="xlsx">Excel</option>
554
+ </select>
555
+ </div>
556
+ <button class="btn btn-primary" onclick="downloadIndices()">Download Results</button>
557
+ </div>
558
+ </div>
559
+ </div>
560
+ </div>
561
+ </div>
562
+
563
+ <!-- Diversity Section -->
564
+ <div id="diversity" class="section" style="display: none;">
565
+ <h2>Diversity Calculation</h2>
566
+ <div class="container mt-4">
567
+ <form id="diversity-form" enctype="multipart/form-data">
568
+ <div class="mb-3">
569
+ <label for="feature_subset" class="form-label">Upload Feature Subset</label>
570
+ <input type="file" class="form-control" id="feature_subset" name="feature_subset" accept=".csv,.txt,.xlsx,.xls,.npz">
571
+ <small class="text-muted">Upload your feature subset matrix (supported formats: CSV, TXT, XLSX, NPZ)</small>
572
+ </div>
573
+
574
+ <div class="mb-3">
575
+ <label for="features" class="form-label">Upload Features (Optional)</label>
576
+ <input type="file" class="form-control" id="features" name="features" accept=".csv,.txt,.xlsx,.xls,.npz">
577
+ <small class="text-muted">Upload your features matrix. It's only for hypersphere overlaping algorithm.</small>
578
+ </div>
579
+
580
+ <div class="mb-3">
581
+ <label for="div_type" class="form-label">Diversity Type</label>
582
+ <select class="form-select" id="div_type" name="div_type">
583
+ <option value="logdet">Log Determinant</option>
584
+ <option value="shannon_entropy">Shannon Entropy</option>
585
+ <option value="explicit_diversity_index">Explicit Diversity Index</option>
586
+ <option value="wdud">WDUD</option>
587
+ <option value="hypersphere_overlap">Hypersphere Overlap</option>
588
+ <option value="gini_coefficient">Gini Coefficient</option>
589
+ </select>
590
+ </div>
591
+
592
+ <div class="mb-3">
593
+ <label for="div_parameters" class="form-label">Additional Parameters (JSON format)</label>
594
+ <textarea class="form-control font-monospace" id="div_parameters" name="div_parameters" rows="5"></textarea>
595
+ <small class="text-muted">Specify normalize, truncation, and cs parameters in JSON format</small>
596
+ </div>
597
+
598
+ <div class="d-grid gap-2">
599
+ <button type="submit" class="btn btn-primary">Calculate Diversity</button>
600
+ </div>
601
+
602
+ <!-- Results section -->
603
+ <div id="diversity-results" class="mt-4" style="display: none;">
604
+ <h3>Results</h3>
605
+ <div class="card">
606
+ <div class="card-body">
607
+ <h5 class="card-title">Diversity Score</h5>
608
+ <p class="card-text" id="diversity-score"></p>
609
+ </div>
610
+ </div>
611
+ </div>
612
+ </form>
613
+ </div>
614
+ </div>
615
+
616
+ <!-- About Section -->
617
+ <div id="about" class="section" style="display: none;">
618
+ <h2>About Selector</h2>
619
+ <div id="about-content">Loading...</div>
620
+ </div>
621
+
622
+ <!-- Contact Section -->
623
+ <div id="contact" class="section" style="display: none;">
624
+ <h2>Contact Information</h2>
625
+ <div id="contact-content">Loading...</div>
626
+ </div>
627
+ </div>
628
+ </div>
629
+
630
+ <!-- Footer -->
631
+ <div class="footer">
632
+ <div class="copyright">
633
+ &copy; Copyright 2017-2024, The QC-Devs Community.
634
+ </div>
635
+ <div class="status">
636
+ <div class="status-indicator"></div>
637
+ <span>Server Status: </span>
638
+ <span id="serverStatus">Loading...</span>
639
+ <div class="status-details">
640
+ <div><strong>Components:</strong></div>
641
+ <div class="component">
642
+ <div class="component-indicator"></div>
643
+ <span>Flask</span>
644
+ </div>
645
+ <div class="component">
646
+ <div class="component-indicator"></div>
647
+ <span>Celery</span>
648
+ </div>
649
+ <div class="component">
650
+ <div class="component-indicator"></div>
651
+ <span>Redis</span>
652
+ </div>
653
+ <div class="resources">
654
+ <div><strong>Resources:</strong></div>
655
+ <div>CPU: <span id="cpuStatus">-</span>%</div>
656
+ <div>Memory: <span id="memoryStatus">-</span>%</div>
657
+ <div>Disk: <span id="diskStatus">-</span>%</div>
658
+ </div>
659
+ </div>
660
+ </div>
661
+ </div>
662
+
663
+ <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script>
664
+ <script>
665
+ let currentIndices = null; // Store the current indices globally
666
+
667
+ // Sidebar toggle functionality
668
+ const sidebar = document.getElementById('sidebar');
669
+ const content = document.getElementById('content');
670
+ const sidebarCollapse = document.getElementById('sidebarCollapse');
671
+ const showSidebarBtn = document.getElementById('showSidebarBtn');
672
+
673
+ function toggleSidebar() {
674
+ sidebar.classList.toggle('active');
675
+ content.classList.toggle('active');
676
+ showSidebarBtn.classList.toggle('show', sidebar.classList.contains('active'));
677
+ }
678
+
679
+ sidebarCollapse.addEventListener('click', toggleSidebar);
680
+ showSidebarBtn.addEventListener('click', toggleSidebar);
681
+
682
+ function showSection(sectionId) {
683
+ // Hide all sections
684
+ document.querySelectorAll('.section').forEach(section => {
685
+ section.style.display = 'none';
686
+ });
687
+
688
+ // Remove active class from all nav links
689
+ document.querySelectorAll('.nav-link').forEach(link => {
690
+ link.classList.remove('active');
691
+ });
692
+
693
+ // Show selected section
694
+ const section = document.getElementById(sectionId);
695
+ if (section) {
696
+ section.style.display = 'block';
697
+ // Add active class to corresponding nav link
698
+ const navLink = document.querySelector(`.nav-link[data-section="${sectionId}"]`);
699
+ if (navLink) {
700
+ navLink.classList.add('active');
701
+ }
702
+ }
703
+ }
704
+
705
+ // Handle navigation clicks
706
+ document.querySelectorAll('.nav-link').forEach(link => {
707
+ link.addEventListener('click', (e) => {
708
+ e.preventDefault();
709
+ const sectionId = link.getAttribute('data-section');
710
+ showSection(sectionId);
711
+ });
712
+ });
713
+
714
+ // Show selection section by default
715
+ document.addEventListener('DOMContentLoaded', function() {
716
+ showSection('selection');
717
+ loadMarkdownContent();
718
+ checkServerStatus();
719
+ setInterval(checkServerStatus, 30000);
720
+ });
721
+
722
+ async function updateDefaultParams() {
723
+ const algorithm = document.getElementById('algorithm').value;
724
+ const parametersTextarea = document.getElementById('parameters');
725
+ const nbinsInput = document.getElementById('nbins_axis');
726
+
727
+ try {
728
+ const response = await fetch(`/get_default_params/${algorithm}`);
729
+ const data = await response.json();
730
+
731
+ if (response.ok) {
732
+ // Get base parameters
733
+ const params = data;
734
+
735
+ // Add nbins_axis for GridPartition
736
+ if (algorithm === 'GridPartition') {
737
+ params.nbins_axis = parseInt(nbinsInput.value) || 5;
738
+ }
739
+
740
+ // Format the parameters nicely
741
+ parametersTextarea.value = JSON.stringify(params, null, 2);
742
+ } else {
743
+ console.error('Error getting default parameters:', data.error);
744
+ parametersTextarea.value = '{}';
745
+ }
746
+ } catch (error) {
747
+ console.error('Error:', error);
748
+ parametersTextarea.value = '{}';
749
+ }
750
+ }
751
+
752
+ // Update parameters when algorithm changes
753
+ document.getElementById('algorithm').addEventListener('change', function() {
754
+ const algorithm = this.value;
755
+ const nbinsContainer = document.getElementById('nbins_axis_container');
756
+
757
+ // Show/hide nbins_axis input based on algorithm
758
+ if (algorithm === 'GridPartition') {
759
+ nbinsContainer.style.display = 'block';
760
+ } else {
761
+ nbinsContainer.style.display = 'none';
762
+ }
763
+
764
+ updateDefaultParams();
765
+ });
766
+
767
+ // Update parameters when nbins_axis changes
768
+ document.getElementById('nbins_axis').addEventListener('input', function() {
769
+ if (document.getElementById('algorithm').value === 'GridPartition') {
770
+ const params = JSON.parse(document.getElementById('parameters').value || '{}');
771
+ params.nbins_axis = parseInt(this.value) || 5;
772
+ document.getElementById('parameters').value = JSON.stringify(params, null, 2);
773
+ }
774
+ });
775
+
776
+ // Update parameters when page loads
777
+ document.addEventListener('DOMContentLoaded', function() {
778
+ updateDefaultParams();
779
+ // Also trigger algorithm change to show/hide nbins_axis input
780
+ const algorithmSelect = document.getElementById('algorithm');
781
+ algorithmSelect.dispatchEvent(new Event('change'));
782
+ });
783
+
784
+ async function downloadIndices() {
785
+ if (!currentIndices) {
786
+ console.error('No indices available for download');
787
+ return;
788
+ }
789
+
790
+ const format = document.getElementById('download-format').value;
791
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
792
+
793
+ try {
794
+ const response = await fetch('/download', {
795
+ method: 'POST',
796
+ headers: {
797
+ 'Content-Type': 'application/json',
798
+ },
799
+ body: JSON.stringify({
800
+ indices: currentIndices,
801
+ format: format,
802
+ timestamp: timestamp
803
+ })
804
+ });
805
+
806
+ if (response.ok) {
807
+ // Create a download link
808
+ const blob = await response.blob();
809
+ const url = window.URL.createObjectURL(blob);
810
+ const a = document.createElement('a');
811
+ a.href = url;
812
+ a.download = `selected_indices_${timestamp}.${format}`;
813
+ document.body.appendChild(a);
814
+ a.click();
815
+ window.URL.revokeObjectURL(url);
816
+ a.remove();
817
+ } else {
818
+ const error = await response.json();
819
+ console.error('Download failed:', error);
820
+ alert('Failed to download results: ' + (error.error || 'Unknown error'));
821
+ }
822
+ } catch (error) {
823
+ console.error('Download error:', error);
824
+ alert('Error downloading results: ' + error.message);
825
+ }
826
+ }
827
+
828
+ document.getElementById('selectionForm').addEventListener('submit', async (e) => {
829
+ e.preventDefault();
830
+
831
+ const formData = new FormData(e.target);
832
+ const resultSection = document.getElementById('resultSection');
833
+ const warningsDiv = document.getElementById('warnings');
834
+ const errorDiv = document.getElementById('error');
835
+ const successContent = document.getElementById('success-content');
836
+ const indicesDiv = document.getElementById('indices');
837
+
838
+ // Validate size is an integer
839
+ const size = parseInt(formData.get('size'));
840
+ if (isNaN(size) || size < 1 || size !== parseFloat(formData.get('size'))) {
841
+ errorDiv.textContent = 'Size must be a positive integer';
842
+ resultSection.style.display = 'block';
843
+ return;
844
+ }
845
+
846
+ try {
847
+ const response = await fetch('/upload_selection', {
848
+ method: 'POST',
849
+ body: formData
850
+ });
851
+
852
+ const data = await response.json();
853
+ console.log('Response data:', data);
854
+
855
+ // Reset display
856
+ resultSection.style.display = 'block';
857
+ warningsDiv.style.display = 'none';
858
+ errorDiv.style.display = 'none';
859
+ successContent.style.display = 'none';
860
+
861
+ if (response.ok && data.success) {
862
+ // Store the indices globally
863
+ currentIndices = data.indices;
864
+
865
+ // Show warnings if any
866
+ if (data.warnings && data.warnings.length > 0) {
867
+ warningsDiv.textContent = 'Warnings: ' + data.warnings.join(', ');
868
+ warningsDiv.style.display = 'block';
869
+ }
870
+
871
+ // Show results
872
+ indicesDiv.textContent = JSON.stringify(data.indices, null, 2);
873
+ successContent.style.display = 'block';
874
+ } else {
875
+ errorDiv.textContent = data.error || 'Failed to process selection';
876
+ errorDiv.style.display = 'block';
877
+ }
878
+ } catch (error) {
879
+ console.error('Error:', error);
880
+ errorDiv.textContent = 'Error processing request: ' + error.message;
881
+ errorDiv.style.display = 'block';
882
+ }
883
+ });
884
+
885
+ // Server status check
886
+ async function checkServerStatus() {
887
+ try {
888
+ const response = await fetch('/status');
889
+ const data = await response.json();
890
+ const statusElem = document.getElementById('serverStatus');
891
+ const indicator = document.querySelector('.status-indicator');
892
+
893
+ // Update main status
894
+ statusElem.textContent = data.status === 'ok' ? 'Running' :
895
+ data.status === 'degraded' ? 'Degraded' : 'Error';
896
+
897
+ // Update indicator color
898
+ indicator.style.backgroundColor =
899
+ data.status === 'ok' ? '#22c55e' : // green
900
+ data.status === 'degraded' ? '#f59e0b' : // orange
901
+ '#ef4444'; // red
902
+
903
+ // Update components
904
+ const components = document.querySelectorAll('.component');
905
+ Object.entries(data.components).forEach((component, index) => {
906
+ const [name, status] = component;
907
+ components[index].querySelector('.component-indicator').style.backgroundColor =
908
+ status ? '#22c55e' : '#ef4444';
909
+ });
910
+ } catch (error) {
911
+ const statusElem = document.getElementById('serverStatus');
912
+ const indicator = document.querySelector('.status-indicator');
913
+ statusElem.textContent = 'Offline';
914
+ indicator.style.backgroundColor = '#ef4444'; // red
915
+
916
+ // Set all components to red when offline
917
+ const components = document.querySelectorAll('.component');
918
+ components.forEach(comp => {
919
+ comp.querySelector('.component-indicator').style.backgroundColor = '#ef4444';
920
+ });
921
+ }
922
+ }
923
+
924
+ // Load markdown content and add MathJax support
925
+ async function loadMarkdownContent() {
926
+ try {
927
+ // Load about content
928
+ const aboutResponse = await fetch('/md/about.md');
929
+ const aboutData = await aboutResponse.json();
930
+ document.getElementById('about-content').innerHTML = aboutData.html;
931
+
932
+ // Load contact content
933
+ const contactResponse = await fetch('/md/contacts.md');
934
+ const contactData = await contactResponse.json();
935
+ document.getElementById('contact-content').innerHTML = contactData.html;
936
+
937
+ // Retypeset math
938
+ if (window.MathJax && window.MathJax.Hub) {
939
+ window.MathJax.Hub.Queue(["Typeset", window.MathJax.Hub]);
940
+ }
941
+ } catch (error) {
942
+ console.error('Error loading markdown content:', error);
943
+ }
944
+ }
945
+
946
+ document.getElementById('diversity-form').addEventListener('submit', async function(e) {
947
+ e.preventDefault();
948
+
949
+ const formData = new FormData(this);
950
+ const parameters = document.getElementById('div_parameters').value;
951
+
952
+ try {
953
+ // Parse and add JSON parameters
954
+ const params = JSON.parse(parameters || '{}');
955
+ formData.set('div_parameters', JSON.stringify(params));
956
+
957
+ const response = await fetch('/calculate_diversity', {
958
+ method: 'POST',
959
+ body: formData
960
+ });
961
+
962
+ const result = await response.json();
963
+
964
+ if (response.ok) {
965
+ // Show results
966
+ document.getElementById('diversity-results').style.display = 'block';
967
+ document.getElementById('diversity-score').textContent = result.diversity_score;
968
+ } else {
969
+ alert('Error: ' + result.error);
970
+ }
971
+ } catch (error) {
972
+ console.error('Error:', error);
973
+ alert('Error calculating diversity: ' + error.message);
974
+ }
975
+ });
976
+
977
+ // Set default parameters when diversity type changes
978
+ document.getElementById('div_type').addEventListener('change', function() {
979
+ const defaultParams = {
980
+ normalize: false,
981
+ truncation: false,
982
+ cs: null
983
+ };
984
+ document.getElementById('div_parameters').value = JSON.stringify(defaultParams, null, 2);
985
+ });
986
+
987
+ // Initialize default parameters
988
+ document.addEventListener('DOMContentLoaded', function() {
989
+ const defaultParams = {
990
+ normalize: false,
991
+ truncation: false,
992
+ cs: null
993
+ };
994
+ document.getElementById('div_parameters').value = JSON.stringify(defaultParams, null, 2);
995
+ });
996
+ </script>
997
+ </body>
998
+ </html>
tests/generate_test_data.py ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+
3
+ # Create a simple 3x3 matrix
4
+ matrix = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0], [7.0, 8.0, 9.0]])
5
+
6
+ # Save two identical copies
7
+ np.savez("matrix1.npz", arr_0=matrix)
8
+ np.savez("matrix2.npz", arr_0=matrix)