legend1234
commited on
Commit
·
b627819
1
Parent(s):
c2d1168
Add initial files
Browse files- Dockerfile +28 -0
- Dockerfile.hf +61 -0
- README.md +53 -11
- app.py +555 -0
- celery_config.py +17 -0
- docker-compose.yml +96 -0
- gunicorn_config.py +55 -0
- md_files/about.md +22 -0
- md_files/contacts.md +2 -0
- nginx.conf +62 -0
- prometheus.yml +20 -0
- requirements.txt +17 -0
- templates/index.html +998 -0
- tests/generate_test_data.py +8 -0
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 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
+
© 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)
|