Spaces:
Running
Running
tiffank1802 commited on
Commit ·
b42075e
1
Parent(s): 13b8d6c
Add Django simulation server with Docker config
Browse files- .dockerignore +37 -0
- Dockerfile +22 -0
- Dockerfile.hfspaces +23 -0
- HF_SPACES_README.md +132 -0
- README.md +4 -6
- bookshop_old/__init__.py +0 -0
- bookshop_old/asgi.py +16 -0
- bookshop_old/settings.py +128 -0
- bookshop_old/templates/admin/base_site.html +13 -0
- bookshop_old/templates/admin/index.html +36 -0
- bookshop_old/urls.py +25 -0
- bookshop_old/wsgi.py +16 -0
- docker-compose.yml +41 -0
- manage.py +22 -0
- requirements.txt +7 -0
- simulations/__init__.py +0 -0
- simulations/admin.py +22 -0
- simulations/apps.py +7 -0
- simulations/gradio_app.py +144 -0
- simulations/management/__init__.py +0 -0
- simulations/management/commands/__init__.py +0 -0
- simulations/management/commands/init_simulation_methods.py +205 -0
- simulations/migrations/0001_initial.py +61 -0
- simulations/migrations/__init__.py +0 -0
- simulations/models.py +515 -0
- simulations/registry.py +181 -0
- simulations/serializers.py +57 -0
- simulations/tasks.py +418 -0
- simulations/templates/simulations/base.html +78 -0
- simulations/templates/simulations/home.html +56 -0
- simulations/templates/simulations/method_detail.html +24 -0
- simulations/templates/simulations/method_list.html +17 -0
- simulations/templates/simulations/run_create.html +57 -0
- simulations/templates/simulations/run_detail.html +193 -0
- simulations/templates/simulations/run_list.html +52 -0
- simulations/templatetags/__init__.py +0 -0
- simulations/templatetags/simulation_extras.py +88 -0
- simulations/urls.py +11 -0
- simulations/urls_views.py +15 -0
- simulations/views.py +85 -0
- simulations/views_views.py +106 -0
- simulationserver/README.md +116 -0
- simulationserver/__init__.py +3 -0
- simulationserver/asgi.py +9 -0
- simulationserver/celery.py +13 -0
- simulationserver/requirements.txt +7 -0
- simulationserver/settings.py +112 -0
- simulationserver/templates/simulations/home.html +113 -0
- simulationserver/urls.py +14 -0
- simulationserver/wsgi.py +9 -0
.dockerignore
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__
|
| 2 |
+
*.py[cod]
|
| 3 |
+
*$py.class
|
| 4 |
+
*.so
|
| 5 |
+
.Python
|
| 6 |
+
build/
|
| 7 |
+
develop-eggs/
|
| 8 |
+
dist/
|
| 9 |
+
downloads/
|
| 10 |
+
eggs/
|
| 11 |
+
.eggs/
|
| 12 |
+
lib/
|
| 13 |
+
lib64/
|
| 14 |
+
parts/
|
| 15 |
+
sdist/
|
| 16 |
+
var/
|
| 17 |
+
wheels/
|
| 18 |
+
*.egg-info/
|
| 19 |
+
.installed.cfg
|
| 20 |
+
*.egg
|
| 21 |
+
.env
|
| 22 |
+
.venv
|
| 23 |
+
env/
|
| 24 |
+
venv/
|
| 25 |
+
ENV/
|
| 26 |
+
.git
|
| 27 |
+
.gitignore
|
| 28 |
+
.pytest_cache
|
| 29 |
+
.coverage
|
| 30 |
+
htmlcov/
|
| 31 |
+
*.log
|
| 32 |
+
.DS_Store
|
| 33 |
+
.vscode
|
| 34 |
+
*.sqlite3
|
| 35 |
+
db.sqlite3
|
| 36 |
+
media/
|
| 37 |
+
staticfiles/
|
Dockerfile
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 4 |
+
ENV PYTHONUNBUFFERED=1
|
| 5 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 6 |
+
|
| 7 |
+
WORKDIR /root/bookshop
|
| 8 |
+
|
| 9 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 10 |
+
gcc \
|
| 11 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 12 |
+
|
| 13 |
+
COPY requirements.txt .
|
| 14 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 15 |
+
|
| 16 |
+
COPY . .
|
| 17 |
+
|
| 18 |
+
RUN python manage.py migrate --run-syncdb 2>/dev/null || true
|
| 19 |
+
|
| 20 |
+
EXPOSE 7860
|
| 21 |
+
|
| 22 |
+
CMD ["sh", "-c", "redis-server --daemonize yes && python manage.py runserver 0.0.0.0:7860"]
|
Dockerfile.hfspaces
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.11-slim
|
| 2 |
+
|
| 3 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 4 |
+
ENV PYTHONUNBUFFERED=1
|
| 5 |
+
ENV DEBIAN_FRONTEND=noninteractive
|
| 6 |
+
ENV HF_HUB_DISABLE_XDG=1
|
| 7 |
+
|
| 8 |
+
WORKDIR /root/bookshop
|
| 9 |
+
|
| 10 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 11 |
+
gcc \
|
| 12 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 13 |
+
|
| 14 |
+
COPY simulationserver/requirements.txt .
|
| 15 |
+
RUN pip install --no-cache-dir -r simulationserver/requirements.txt
|
| 16 |
+
|
| 17 |
+
COPY . .
|
| 18 |
+
|
| 19 |
+
RUN python manage.py migrate --run-syncdb 2>/dev/null || true
|
| 20 |
+
|
| 21 |
+
EXPOSE 7860
|
| 22 |
+
|
| 23 |
+
CMD ["sh", "-c", "python manage.py runserver 0.0.0.0:7860"]
|
HF_SPACES_README.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Simulation Server - HuggingFace Spaces Deployment
|
| 2 |
+
|
| 3 |
+
## Overview
|
| 4 |
+
|
| 5 |
+
This project is a Django web application for running numerical simulations with asynchronous processing via Celery and Redis.
|
| 6 |
+
|
| 7 |
+
## HuggingFace Spaces Configuration
|
| 8 |
+
|
| 9 |
+
### Option 1: Using Gradio Interface (Recommended)
|
| 10 |
+
|
| 11 |
+
Create a new file `simulations/gradio_app.py`:
|
| 12 |
+
|
| 13 |
+
```python
|
| 14 |
+
import gradio as gr
|
| 15 |
+
import requests
|
| 16 |
+
|
| 17 |
+
def run_simulation(method, **params):
|
| 18 |
+
response = requests.post(
|
| 19 |
+
"http://localhost:8000/api/runs/",
|
| 20 |
+
json={"method": method, "parameters": params}
|
| 21 |
+
)
|
| 22 |
+
return response.json()
|
| 23 |
+
|
| 24 |
+
# Create Gradio interface...
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
### Option 2: Using Docker (Full Features)
|
| 28 |
+
|
| 29 |
+
1. **Create a new Space on HuggingFace**
|
| 30 |
+
- Go to https://huggingface.co/new-space
|
| 31 |
+
- Choose "Docker" as the SDK
|
| 32 |
+
- Set hardware to "CPU" or "GPU" as needed
|
| 33 |
+
|
| 34 |
+
2. **Push your code to HuggingFace**
|
| 35 |
+
```bash
|
| 36 |
+
git add .
|
| 37 |
+
git commit -m "Add HuggingFace deployment"
|
| 38 |
+
git remote add hf https://huggingface.co/username/space-name
|
| 39 |
+
git push hf main
|
| 40 |
+
```
|
| 41 |
+
|
| 42 |
+
3. **Create `HF_Dockerfile`** in your repository root:
|
| 43 |
+
```dockerfile
|
| 44 |
+
FROM python:3.11-slim
|
| 45 |
+
|
| 46 |
+
ENV PYTHONDONTWRITEBYTECODE=1
|
| 47 |
+
ENV PYTHONUNBUFFERED=1
|
| 48 |
+
|
| 49 |
+
WORKDIR /root/bookshop
|
| 50 |
+
|
| 51 |
+
RUN apt-get update && apt-get install -y --no-install-recommends gcc
|
| 52 |
+
RUN rm -rf /var/lib/apt/lists/*
|
| 53 |
+
|
| 54 |
+
COPY simulationserver/requirements.txt .
|
| 55 |
+
RUN pip install --no-cache-dir -r simulationserver/requirements.txt
|
| 56 |
+
|
| 57 |
+
COPY . .
|
| 58 |
+
|
| 59 |
+
EXPOSE 7860
|
| 60 |
+
|
| 61 |
+
CMD ["python", "manage.py", "runserver", "0.0.0.0:7860"]
|
| 62 |
+
```
|
| 63 |
+
|
| 64 |
+
4. **Update `simulationserver/settings.py`** for HuggingFace:
|
| 65 |
+
```python
|
| 66 |
+
# Use environment variables
|
| 67 |
+
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'your-secret-key')
|
| 68 |
+
DEBUG = os.environ.get('DEBUG', 'False').lower() in ('true', '1')
|
| 69 |
+
```
|
| 70 |
+
|
| 71 |
+
## Important Notes for HuggingFace Spaces
|
| 72 |
+
|
| 73 |
+
### Limitations
|
| 74 |
+
- **No persistent Redis**: Celery workers won't work with persistent queues
|
| 75 |
+
- **No background tasks**: Jobs are limited to the container lifetime
|
| 76 |
+
- **Ephemeral storage**: Files are lost when the space restarts
|
| 77 |
+
|
| 78 |
+
### Recommended: Use Synchronous Mode
|
| 79 |
+
|
| 80 |
+
For HuggingFace Spaces, modify `simulations/tasks.py` to run synchronously:
|
| 81 |
+
|
| 82 |
+
```python
|
| 83 |
+
def run_simulation_sync(method_slug, params):
|
| 84 |
+
"""Run simulation synchronously for HuggingFace Spaces."""
|
| 85 |
+
method_func = SIMULATION_METHODS.get(method_slug)
|
| 86 |
+
if not method_func:
|
| 87 |
+
raise ValueError(f"Méthode inconnue: {method_slug}")
|
| 88 |
+
|
| 89 |
+
result = None
|
| 90 |
+
for progress in method_func(params):
|
| 91 |
+
yield progress
|
| 92 |
+
if progress >= 100:
|
| 93 |
+
break
|
| 94 |
+
return result
|
| 95 |
+
```
|
| 96 |
+
|
| 97 |
+
### Alternative: Use External Redis
|
| 98 |
+
|
| 99 |
+
For full Celery support, use an external Redis service:
|
| 100 |
+
|
| 101 |
+
```python
|
| 102 |
+
CELERY_BROKER_URL = os.environ.get('REDIS_URL', 'redis://localhost:6379/0')
|
| 103 |
+
```
|
| 104 |
+
|
| 105 |
+
## Local Development with Docker
|
| 106 |
+
|
| 107 |
+
```bash
|
| 108 |
+
# Build and run
|
| 109 |
+
docker build -t simulation-server .
|
| 110 |
+
docker run -p 8000:8000 simulation-server
|
| 111 |
+
|
| 112 |
+
# With docker-compose
|
| 113 |
+
docker-compose up -d
|
| 114 |
+
```
|
| 115 |
+
|
| 116 |
+
## Environment Variables
|
| 117 |
+
|
| 118 |
+
| Variable | Description | Default |
|
| 119 |
+
|----------|-------------|---------|
|
| 120 |
+
| `DJANGO_SECRET_KEY` | Django secret key | `django-insecure-...` |
|
| 121 |
+
| `DEBUG` | Debug mode | `True` |
|
| 122 |
+
| `CELERY_BROKER_URL` | Redis broker URL | `redis://localhost:6379/0` |
|
| 123 |
+
|
| 124 |
+
## API Endpoints
|
| 125 |
+
|
| 126 |
+
- `GET /` - Homepage
|
| 127 |
+
- `GET /methods/` - List simulation methods
|
| 128 |
+
- `GET /run/create/` - Create new simulation
|
| 129 |
+
- `GET /runs/` - List simulation history
|
| 130 |
+
- `GET /run/{id}/` - View simulation results
|
| 131 |
+
- `POST /api/runs/` - Create simulation via API
|
| 132 |
+
- `GET /api/runs/{id}/` - Get simulation status via API
|
README.md
CHANGED
|
@@ -3,12 +3,10 @@ title: Simulations
|
|
| 3 |
emoji: 🐠
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: indigo
|
| 6 |
-
sdk:
|
| 7 |
-
sdk_version:
|
| 8 |
-
app_file:
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
-
short_description: simulations
|
| 12 |
---
|
| 13 |
-
|
| 14 |
-
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 3 |
emoji: 🐠
|
| 4 |
colorFrom: red
|
| 5 |
colorTo: indigo
|
| 6 |
+
sdk: docker
|
| 7 |
+
sdk_version: latest
|
| 8 |
+
app_file: Dockerfile
|
| 9 |
pinned: false
|
| 10 |
license: mit
|
| 11 |
+
short_description: Numerical simulations with Django + Celery + Redis
|
| 12 |
---
|
|
|
|
|
|
bookshop_old/__init__.py
ADDED
|
File without changes
|
bookshop_old/asgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ASGI config for bookshop project.
|
| 3 |
+
|
| 4 |
+
It exposes the ASGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/6.0/howto/deployment/asgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.asgi import get_asgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bookshop.settings')
|
| 15 |
+
|
| 16 |
+
application = get_asgi_application()
|
bookshop_old/settings.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Django settings for bookshop project.
|
| 3 |
+
|
| 4 |
+
Generated by 'django-admin startproject' using Django 6.0.1.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/6.0/topics/settings/
|
| 8 |
+
|
| 9 |
+
For the full list of settings and their values, see
|
| 10 |
+
https://docs.djangoproject.com/en/6.0/ref/settings/
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
import sys
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
import os
|
| 17 |
+
sys.path.insert(0, str(Path.home() / 'django-polls'))
|
| 18 |
+
|
| 19 |
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
| 20 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
# Quick-start development settings - unsuitable for production
|
| 24 |
+
# See https://docs.djangoproject.com/en/6.0/howto/deployment/checklist/
|
| 25 |
+
|
| 26 |
+
# SECURITY WARNING: keep the secret key used in production secret!
|
| 27 |
+
SECRET_KEY = 'django-insecure-2#632)2^e*@ga!x@$mj^6=u7dj)2t#gq^t8)2t3!1jr0o(9he0'
|
| 28 |
+
|
| 29 |
+
# SECURITY WARNING: don't run with debug turned on in production!
|
| 30 |
+
DEBUG = True
|
| 31 |
+
|
| 32 |
+
ALLOWED_HOSTS = ['testserver.com', 'testserver','localhost', ]
|
| 33 |
+
|
| 34 |
+
INTERNAL_IPS = [
|
| 35 |
+
"127.0.0.1",
|
| 36 |
+
]
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
# Application definition
|
| 40 |
+
|
| 41 |
+
INSTALLED_APPS = [
|
| 42 |
+
'django_polls.apps.PollsConfig',
|
| 43 |
+
'django.contrib.admin',
|
| 44 |
+
'django.contrib.auth',
|
| 45 |
+
'django.contrib.contenttypes',
|
| 46 |
+
'django.contrib.sessions',
|
| 47 |
+
'django.contrib.messages',
|
| 48 |
+
'django.contrib.staticfiles',
|
| 49 |
+
'debug_toolbar',
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
MIDDLEWARE = [
|
| 53 |
+
'django.middleware.security.SecurityMiddleware',
|
| 54 |
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
| 55 |
+
'django.middleware.common.CommonMiddleware',
|
| 56 |
+
'django.middleware.csrf.CsrfViewMiddleware',
|
| 57 |
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
| 58 |
+
'django.contrib.messages.middleware.MessageMiddleware',
|
| 59 |
+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
| 60 |
+
'debug_toolbar.middleware.DebugToolbarMiddleware',
|
| 61 |
+
]
|
| 62 |
+
|
| 63 |
+
ROOT_URLCONF = 'bookshop.urls'
|
| 64 |
+
|
| 65 |
+
TEMPLATES = [
|
| 66 |
+
{
|
| 67 |
+
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
| 68 |
+
"DIRS": [BASE_DIR / "templates"],
|
| 69 |
+
"APP_DIRS": True,
|
| 70 |
+
"OPTIONS": {
|
| 71 |
+
"context_processors": [
|
| 72 |
+
"django.template.context_processors.request",
|
| 73 |
+
"django.contrib.auth.context_processors.auth",
|
| 74 |
+
"django.contrib.messages.context_processors.messages",
|
| 75 |
+
],
|
| 76 |
+
},
|
| 77 |
+
},
|
| 78 |
+
]
|
| 79 |
+
|
| 80 |
+
WSGI_APPLICATION = 'bookshop.wsgi.application'
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
# Database
|
| 84 |
+
# https://docs.djangoproject.com/en/6.0/ref/settings/#databases
|
| 85 |
+
|
| 86 |
+
DATABASES = {
|
| 87 |
+
'default': {
|
| 88 |
+
'ENGINE': 'django.db.backends.sqlite3',
|
| 89 |
+
'NAME': BASE_DIR / 'db.sqlite3',
|
| 90 |
+
}
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
# Password validation
|
| 95 |
+
# https://docs.djangoproject.com/en/6.0/ref/settings/#auth-password-validators
|
| 96 |
+
|
| 97 |
+
AUTH_PASSWORD_VALIDATORS = [
|
| 98 |
+
{
|
| 99 |
+
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
| 103 |
+
},
|
| 104 |
+
{
|
| 105 |
+
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
| 109 |
+
},
|
| 110 |
+
]
|
| 111 |
+
|
| 112 |
+
|
| 113 |
+
# Internationalization
|
| 114 |
+
# https://docs.djangoproject.com/en/6.0/topics/i18n/
|
| 115 |
+
|
| 116 |
+
LANGUAGE_CODE = 'en-us'
|
| 117 |
+
|
| 118 |
+
TIME_ZONE = 'UTC'
|
| 119 |
+
|
| 120 |
+
USE_I18N = True
|
| 121 |
+
|
| 122 |
+
USE_TZ = True
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
# Static files (CSS, JavaScript, Images)
|
| 126 |
+
# https://docs.djangoproject.com/en/6.0/howto/static-files/
|
| 127 |
+
|
| 128 |
+
STATIC_URL = 'static/'
|
bookshop_old/templates/admin/base_site.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "admin/base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}{% if subtitle %}{{ subtitle }} | {% endif %}{{ title }} | {{ site_title|default:_('Polls Administration') }}{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block branding %}
|
| 6 |
+
<div id="site-name"><a href="{% url 'admin:index' %}">{{ site_header|default:_('Polls Administration') }}</a></div>
|
| 7 |
+
{% if user.is_anonymous %}
|
| 8 |
+
{% include "admin/color_theme_toggle.html" %}
|
| 9 |
+
{% endif %}
|
| 10 |
+
{% endblock %}
|
| 11 |
+
|
| 12 |
+
{% block nav-global %}{% endblock %}
|
| 13 |
+
|
bookshop_old/templates/admin/index.html
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "admin/index.html" %}
|
| 2 |
+
{% load i18n admin_static %}
|
| 3 |
+
|
| 4 |
+
{% block bodyclass %}{{ block.super }} app-polls model-question change-list{% endblock %}
|
| 5 |
+
|
| 6 |
+
{% if not is_popup %}
|
| 7 |
+
{% block breadcrumbs %}
|
| 8 |
+
<div class="breadcrumbs">
|
| 9 |
+
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
|
| 10 |
+
> <a href="{% url 'admin:app_list' app_label='polls' %}">Polls</a>
|
| 11 |
+
> {% trans 'Questions' %}
|
| 12 |
+
</div>
|
| 13 |
+
{% endblock %}
|
| 14 |
+
{% endif %}
|
| 15 |
+
|
| 16 |
+
{% block content %}
|
| 17 |
+
<div id="content-main">
|
| 18 |
+
<h1>{% trans 'Recent Questions' %}</h1>
|
| 19 |
+
{% if latest_question_list %}
|
| 20 |
+
<table>
|
| 21 |
+
<tr>
|
| 22 |
+
<th>Question</th>
|
| 23 |
+
<th>Published</th>
|
| 24 |
+
</tr>
|
| 25 |
+
{% for question in latest_question_list %}
|
| 26 |
+
<tr>
|
| 27 |
+
<td><a href="{% url 'admin:polls_question_change' question.id %}">{{ question.question_text }}</a></td>
|
| 28 |
+
<td>{{ question.pub_date|date:"SHORT_DATE_FORMAT" }}</td>
|
| 29 |
+
</tr>
|
| 30 |
+
{% endfor %}
|
| 31 |
+
</table>
|
| 32 |
+
{% else %}
|
| 33 |
+
<p>{% trans 'No questions have been published yet.' %}</p>
|
| 34 |
+
{% endif %}
|
| 35 |
+
</div>
|
| 36 |
+
{% endblock %}
|
bookshop_old/urls.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
URL configuration for bookshop project.
|
| 3 |
+
|
| 4 |
+
The `urlpatterns` list routes URLs to views. For more information please see:
|
| 5 |
+
https://docs.djangoproject.com/en/6.0/topics/http/urls/
|
| 6 |
+
Examples:
|
| 7 |
+
Function views
|
| 8 |
+
1. Add an import: from my_app import views
|
| 9 |
+
2. Add a URL to urlpatterns: path('', views.home, name='home')
|
| 10 |
+
Class-based views
|
| 11 |
+
1. Add an import: from other_app.views import Home
|
| 12 |
+
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
|
| 13 |
+
Including another URLconf
|
| 14 |
+
1. Import the include() function: from django.urls import include, path
|
| 15 |
+
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
| 16 |
+
"""
|
| 17 |
+
from django.contrib import admin
|
| 18 |
+
from django.urls import path,include
|
| 19 |
+
from debug_toolbar.toolbar import debug_toolbar_urls
|
| 20 |
+
|
| 21 |
+
urlpatterns = [
|
| 22 |
+
path('admin/', admin.site.urls),
|
| 23 |
+
path('polls/', include('django_polls.urls')),
|
| 24 |
+
|
| 25 |
+
] + debug_toolbar_urls()
|
bookshop_old/wsgi.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WSGI config for bookshop project.
|
| 3 |
+
|
| 4 |
+
It exposes the WSGI callable as a module-level variable named ``application``.
|
| 5 |
+
|
| 6 |
+
For more information on this file, see
|
| 7 |
+
https://docs.djangoproject.com/en/6.0/howto/deployment/wsgi/
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
import os
|
| 11 |
+
|
| 12 |
+
from django.core.wsgi import get_wsgi_application
|
| 13 |
+
|
| 14 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'bookshop.settings')
|
| 15 |
+
|
| 16 |
+
application = get_wsgi_application()
|
docker-compose.yml
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version: '3.8'
|
| 2 |
+
|
| 3 |
+
services:
|
| 4 |
+
redis:
|
| 5 |
+
image: redis:7-alpine
|
| 6 |
+
ports:
|
| 7 |
+
- "6379:6379"
|
| 8 |
+
volumes:
|
| 9 |
+
- redis_data:/data
|
| 10 |
+
|
| 11 |
+
web:
|
| 12 |
+
build: .
|
| 13 |
+
ports:
|
| 14 |
+
- "8000:8000"
|
| 15 |
+
volumes:
|
| 16 |
+
- .:/root/bookshop
|
| 17 |
+
- media_data:/root/bookshop/media
|
| 18 |
+
environment:
|
| 19 |
+
- CELERY_BROKER_URL=redis://redis:6379/0
|
| 20 |
+
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
| 21 |
+
- DEBUG=0
|
| 22 |
+
depends_on:
|
| 23 |
+
- redis
|
| 24 |
+
|
| 25 |
+
celery:
|
| 26 |
+
build: .
|
| 27 |
+
volumes:
|
| 28 |
+
- .:/root/bookshop
|
| 29 |
+
- media_data:/root/bookshop/media
|
| 30 |
+
environment:
|
| 31 |
+
- CELERY_BROKER_URL=redis://redis:6379/0
|
| 32 |
+
- CELERY_RESULT_BACKEND=redis://redis:6379/0
|
| 33 |
+
- DEBUG=0
|
| 34 |
+
command: celery -A simulationserver worker --loglevel=info --concurrency=2
|
| 35 |
+
depends_on:
|
| 36 |
+
- redis
|
| 37 |
+
- web
|
| 38 |
+
|
| 39 |
+
volumes:
|
| 40 |
+
redis_data:
|
| 41 |
+
media_data:
|
manage.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python
|
| 2 |
+
"""Django's command-line utility for administrative tasks."""
|
| 3 |
+
import os
|
| 4 |
+
import sys
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def main():
|
| 8 |
+
"""Run administrative tasks."""
|
| 9 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'simulationserver.settings')
|
| 10 |
+
try:
|
| 11 |
+
from django.core.management import execute_from_command_line
|
| 12 |
+
except ImportError as exc:
|
| 13 |
+
raise ImportError(
|
| 14 |
+
"Couldn't import Django. Are you sure it's installed and "
|
| 15 |
+
"available on your PYTHONPATH environment variable? Did you "
|
| 16 |
+
"forget to activate a virtual environment?"
|
| 17 |
+
) from exc
|
| 18 |
+
execute_from_command_line(sys.argv)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
if __name__ == '__main__':
|
| 22 |
+
main()
|
requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Django>=4.2,<5.0
|
| 2 |
+
djangorestframework>=3.14,<4.0
|
| 3 |
+
celery>=5.3,<6.0
|
| 4 |
+
redis>=4.5,<6.0
|
| 5 |
+
numpy>=1.24,<2.0
|
| 6 |
+
matplotlib>=3.7,<4.0
|
| 7 |
+
reportlab>=4.0,<5.0
|
simulations/__init__.py
ADDED
|
File without changes
|
simulations/admin.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.contrib import admin
|
| 2 |
+
from .models import SimulationMethod, SimulationRun
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
@admin.register(SimulationMethod)
|
| 6 |
+
class SimulationMethodAdmin(admin.ModelAdmin):
|
| 7 |
+
list_display = ['name', 'slug', 'is_active', 'created_at']
|
| 8 |
+
list_filter = ['is_active', 'created_at']
|
| 9 |
+
search_fields = ['name', 'description']
|
| 10 |
+
prepopulated_fields = {'slug': ('name',)}
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@admin.register(SimulationRun)
|
| 14 |
+
class SimulationRunAdmin(admin.ModelAdmin):
|
| 15 |
+
list_display = ['id', 'method', 'status', 'progress', 'created_at', 'completed_at']
|
| 16 |
+
list_filter = ['status', 'method', 'created_at']
|
| 17 |
+
search_fields = ['name']
|
| 18 |
+
raw_id_fields = ['method']
|
| 19 |
+
readonly_fields = [
|
| 20 |
+
'status', 'progress', 'logs', 'result_data',
|
| 21 |
+
'error_message', 'started_at', 'completed_at'
|
| 22 |
+
]
|
simulations/apps.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.apps import AppConfig
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class SimulationsConfig(AppConfig):
|
| 5 |
+
default_auto_field = 'django.db.models.BigAutoField'
|
| 6 |
+
name = 'simulations'
|
| 7 |
+
verbose_name = 'Simulations Numériques'
|
simulations/gradio_app.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
from simulations.tasks import SIMULATION_METHODS
|
| 5 |
+
|
| 6 |
+
def get_method_schema(method_slug):
|
| 7 |
+
"""Get parameter schema for a simulation method."""
|
| 8 |
+
from simulations.models import SimulationMethod
|
| 9 |
+
try:
|
| 10 |
+
method = SimulationMethod.objects.get(slug=method_slug)
|
| 11 |
+
return method.parameters_schema
|
| 12 |
+
except SimulationMethod.DoesNotExist:
|
| 13 |
+
return {}
|
| 14 |
+
|
| 15 |
+
def create_param_inputs(method_slug):
|
| 16 |
+
"""Create Gradio input components based on method parameters."""
|
| 17 |
+
schema = get_method_schema(method_slug)
|
| 18 |
+
inputs = []
|
| 19 |
+
for param_name, param_info in schema.get('properties', {}).items():
|
| 20 |
+
default = param_info.get('default', 100)
|
| 21 |
+
min_val = param_info.get('minimum', 0)
|
| 22 |
+
max_val = param_info.get('maximum', 1000)
|
| 23 |
+
step = param_info.get('step', 1)
|
| 24 |
+
inputs.append(
|
| 25 |
+
gr.Slider(
|
| 26 |
+
minimum=min_val,
|
| 27 |
+
maximum=max_val,
|
| 28 |
+
value=default,
|
| 29 |
+
step=step,
|
| 30 |
+
label=param_info.get('title', param_name)
|
| 31 |
+
)
|
| 32 |
+
)
|
| 33 |
+
return inputs
|
| 34 |
+
|
| 35 |
+
def run_simulation_gradio(method_slug, *args):
|
| 36 |
+
"""Run simulation and return results."""
|
| 37 |
+
from simulations.models import SimulationMethod, SimulationRun
|
| 38 |
+
|
| 39 |
+
try:
|
| 40 |
+
method = SimulationMethod.objects.get(slug=method_slug)
|
| 41 |
+
except SimulationMethod.DoesNotExist:
|
| 42 |
+
return f"Erreur: Méthode '{method_slug}' non trouvée"
|
| 43 |
+
|
| 44 |
+
params = {}
|
| 45 |
+
schema = get_method_schema(method_slug)
|
| 46 |
+
properties = schema.get('properties', {})
|
| 47 |
+
|
| 48 |
+
for i, (param_name, param_info) in enumerate(properties.items()):
|
| 49 |
+
if i < len(args):
|
| 50 |
+
params[param_name] = args[i]
|
| 51 |
+
|
| 52 |
+
run = SimulationRun.objects.create(
|
| 53 |
+
method=method,
|
| 54 |
+
name=f"Gradio - {method.name}",
|
| 55 |
+
parameters=params
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
method_func = SIMULATION_METHODS.get(method_slug)
|
| 59 |
+
if not method_func:
|
| 60 |
+
return f"Erreur: Méthode '{method_slug}' non implémentée"
|
| 61 |
+
|
| 62 |
+
try:
|
| 63 |
+
gen = method_func(params)
|
| 64 |
+
result = None
|
| 65 |
+
for progress in gen:
|
| 66 |
+
yield f"Progression: {progress}%"
|
| 67 |
+
result = progress if 'progress' in dir() else {}
|
| 68 |
+
|
| 69 |
+
run.set_success(result or {})
|
| 70 |
+
run.refresh_from_db()
|
| 71 |
+
|
| 72 |
+
output = f"Simulation terminée!\n\n"
|
| 73 |
+
output += f"Méthode: {method.name}\n"
|
| 74 |
+
output += f"ID: {run.id}\n\n"
|
| 75 |
+
|
| 76 |
+
if run.result_data:
|
| 77 |
+
output += "Résultats:\n"
|
| 78 |
+
for key, value in run.result_data.items():
|
| 79 |
+
if isinstance(value, (int, float)):
|
| 80 |
+
output += f" {key}: {value:.4f}\n"
|
| 81 |
+
elif isinstance(value, list) and len(value) < 10:
|
| 82 |
+
output += f" {key}: {value}\n"
|
| 83 |
+
else:
|
| 84 |
+
output += f" {key}: [{type(value).__name__}]\n"
|
| 85 |
+
|
| 86 |
+
if run.plot_file:
|
| 87 |
+
output += f"\nGraphique: {run.plot_file.url}"
|
| 88 |
+
|
| 89 |
+
return output
|
| 90 |
+
|
| 91 |
+
except Exception as e:
|
| 92 |
+
run.set_failure(str(e))
|
| 93 |
+
return f"Erreur: {str(e)}"
|
| 94 |
+
|
| 95 |
+
def update_inputs(method_slug):
|
| 96 |
+
return create_param_inputs(method_slug)
|
| 97 |
+
|
| 98 |
+
def create_gradio_app():
|
| 99 |
+
"""Create the Gradio interface."""
|
| 100 |
+
with gr.Blocks(title="Simulation Numérique") as app:
|
| 101 |
+
gr.Markdown("# Simulation Numérique")
|
| 102 |
+
gr.Markdown("Sélectionnez une méthode et configurez les paramètres.")
|
| 103 |
+
|
| 104 |
+
with gr.Row():
|
| 105 |
+
with gr.Column():
|
| 106 |
+
method_dropdown = gr.Dropdown(
|
| 107 |
+
choices=list(SIMULATION_METHODS.keys()),
|
| 108 |
+
value='monte-carlo-pi',
|
| 109 |
+
label="Méthode de simulation"
|
| 110 |
+
)
|
| 111 |
+
|
| 112 |
+
param_container = gr.Column()
|
| 113 |
+
with param_container:
|
| 114 |
+
initial_inputs = create_param_inputs('monte-carlo-pi')
|
| 115 |
+
|
| 116 |
+
run_button = gr.Button("Lancer la simulation", variant="primary")
|
| 117 |
+
|
| 118 |
+
with gr.Column():
|
| 119 |
+
output = gr.Textbox(
|
| 120 |
+
label="Résultat",
|
| 121 |
+
lines=20,
|
| 122 |
+
interactive=False
|
| 123 |
+
)
|
| 124 |
+
|
| 125 |
+
def on_method_change(method_slug):
|
| 126 |
+
return create_param_inputs(method_slug)
|
| 127 |
+
|
| 128 |
+
method_dropdown.change(
|
| 129 |
+
fn=on_method_change,
|
| 130 |
+
inputs=method_dropdown,
|
| 131 |
+
outputs=param_container
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
run_button.click(
|
| 135 |
+
fn=run_simulation_gradio,
|
| 136 |
+
inputs=[method_dropdown] + initial_inputs,
|
| 137 |
+
outputs=output
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
return app
|
| 141 |
+
|
| 142 |
+
if __name__ == "__main__":
|
| 143 |
+
app = create_gradio_app()
|
| 144 |
+
app.launch()
|
simulations/management/__init__.py
ADDED
|
File without changes
|
simulations/management/commands/__init__.py
ADDED
|
File without changes
|
simulations/management/commands/init_simulation_methods.py
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.core.management.base import BaseCommand
|
| 2 |
+
from simulations.models import SimulationMethod
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class Command(BaseCommand):
|
| 6 |
+
help = 'Initialise les méthodes de simulation par défaut'
|
| 7 |
+
|
| 8 |
+
def handle(self, *args, **options):
|
| 9 |
+
methods = [
|
| 10 |
+
{
|
| 11 |
+
'name': 'Monte Carlo - Estimation de Pi',
|
| 12 |
+
'slug': 'monte-carlo-pi',
|
| 13 |
+
'description': 'Estimation de Pi par la méthode de Monte Carlo.',
|
| 14 |
+
'theory': '''
|
| 15 |
+
## Monte Carlo pour Pi
|
| 16 |
+
|
| 17 |
+
1. Générer N points aléatoires dans un carré
|
| 18 |
+
2. Compter les points dans le cercle
|
| 19 |
+
3. π ≈ 4 × points_cercle / N
|
| 20 |
+
'''.strip(),
|
| 21 |
+
'parameters_schema': {
|
| 22 |
+
'type': 'object',
|
| 23 |
+
'properties': {
|
| 24 |
+
'n_points': {'type': 'integer', 'minimum': 100, 'maximum': 10000000},
|
| 25 |
+
'seed': {'type': 'integer'}
|
| 26 |
+
},
|
| 27 |
+
'required': ['n_points']
|
| 28 |
+
},
|
| 29 |
+
'default_parameters': {'n_points': 10000, 'seed': 42}
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
'name': 'Diffusion 1D',
|
| 33 |
+
'slug': 'diffusion-1d',
|
| 34 |
+
'description': 'Résolution de l\'équation de diffusion en 1D par schéma explicite.',
|
| 35 |
+
'theory': '''
|
| 36 |
+
## Diffusion 1D
|
| 37 |
+
|
| 38 |
+
∂u/∂t = D × ∂²u/∂x²
|
| 39 |
+
|
| 40 |
+
Schéma explicite avec stabilité si D×dt/dx² ≤ 0.5
|
| 41 |
+
'''.strip(),
|
| 42 |
+
'parameters_schema': {
|
| 43 |
+
'type': 'object',
|
| 44 |
+
'properties': {
|
| 45 |
+
'length': {'type': 'number', 'minimum': 0.1, 'maximum': 100},
|
| 46 |
+
'time': {'type': 'number', 'minimum': 0.01, 'maximum': 1000},
|
| 47 |
+
'nx': {'type': 'integer', 'minimum': 10, 'maximum': 10000},
|
| 48 |
+
'nt': {'type': 'integer', 'minimum': 10, 'maximum': 100000},
|
| 49 |
+
'diffusion_coef': {'type': 'number', 'minimum': 0.001, 'maximum': 10}
|
| 50 |
+
}
|
| 51 |
+
},
|
| 52 |
+
'default_parameters': {'length': 1.0, 'time': 0.1, 'nx': 50, 'nt': 100, 'diffusion_coef': 1.0}
|
| 53 |
+
},
|
| 54 |
+
{
|
| 55 |
+
'name': 'Résolution Système Linéaire',
|
| 56 |
+
'slug': 'linear-solve',
|
| 57 |
+
'description': 'Résolution de Ax = b par décomposition LU.',
|
| 58 |
+
'theory': '''
|
| 59 |
+
## Système Linéaire
|
| 60 |
+
|
| 61 |
+
Résolution de Ax = b avec décomposition LU.
|
| 62 |
+
'''.strip(),
|
| 63 |
+
'parameters_schema': {
|
| 64 |
+
'type': 'object',
|
| 65 |
+
'properties': {
|
| 66 |
+
'size': {'type': 'integer', 'minimum': 10, 'maximum': 5000},
|
| 67 |
+
'seed': {'type': 'integer'}
|
| 68 |
+
}
|
| 69 |
+
},
|
| 70 |
+
'default_parameters': {'size': 100, 'seed': 42}
|
| 71 |
+
},
|
| 72 |
+
{
|
| 73 |
+
'name': 'Conduction Thermique',
|
| 74 |
+
'slug': 'heat-conduction',
|
| 75 |
+
'description': 'Simulation de conduction thermique avec différentes méthodes theta (Newton, finite_difference).',
|
| 76 |
+
'theory': '''
|
| 77 |
+
## Conduction Thermique
|
| 78 |
+
|
| 79 |
+
Évolution de la température avec le temps:
|
| 80 |
+
T[n+1] = (1 - (1-θ)×k×dt)×T[n] / (1+θ×k×dt) - k×dt×Ts / (1+θ×k×dt)
|
| 81 |
+
|
| 82 |
+
- θ = 0: Explicite
|
| 83 |
+
- θ = 0.5: Crank-Nicolson
|
| 84 |
+
- θ = 1: Implicite
|
| 85 |
+
'''.strip(),
|
| 86 |
+
'parameters_schema': {
|
| 87 |
+
'type': 'object',
|
| 88 |
+
'properties': {
|
| 89 |
+
'T_initial': {'type': 'number', 'minimum': 0, 'maximum': 1000},
|
| 90 |
+
'T_surface': {'type': 'number', 'minimum': 0, 'maximum': 1000},
|
| 91 |
+
'conductivity': {'type': 'number', 'minimum': 0.001, 'maximum': 10},
|
| 92 |
+
'dt': {'type': 'number', 'minimum': 0.0001, 'maximum': 1},
|
| 93 |
+
'N': {'type': 'integer', 'minimum': 100, 'maximum': 10000},
|
| 94 |
+
'theta': {'type': 'number', 'minimum': 0, 'maximum': 1.2}
|
| 95 |
+
}
|
| 96 |
+
},
|
| 97 |
+
'default_parameters': {'T_initial': 100.0, 'T_surface': 20.0, 'conductivity': 0.1, 'dt': 0.001, 'N': 1000, 'theta': 0.5}
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
'name': 'Flux de Trafic',
|
| 101 |
+
'slug': 'traffic-flow',
|
| 102 |
+
'description': 'Modèle de trafic 1D avec choc (red light) - numerical-mooc.',
|
| 103 |
+
'theory': '''
|
| 104 |
+
## Modèle de Trafic
|
| 105 |
+
|
| 106 |
+
LWR (Lighthill-Whitham-Richards):
|
| 107 |
+
∂ρ/∂t + ∂F/∂x = 0, avec F = ρ × u_max × (1 - ρ/ρ_max)
|
| 108 |
+
|
| 109 |
+
Condition initiale: choc à x=3.0 (rouge)
|
| 110 |
+
'''.strip(),
|
| 111 |
+
'parameters_schema': {
|
| 112 |
+
'type': 'object',
|
| 113 |
+
'properties': {
|
| 114 |
+
'nx': {'type': 'integer', 'minimum': 10, 'maximum': 500},
|
| 115 |
+
'nt': {'type': 'integer', 'minimum': 50, 'maximum': 1000},
|
| 116 |
+
'length': {'type': 'number', 'minimum': 1, 'maximum': 100},
|
| 117 |
+
'time': {'type': 'number', 'minimum': 0.1, 'maximum': 10},
|
| 118 |
+
'u_max': {'type': 'number', 'minimum': 0.1, 'maximum': 10},
|
| 119 |
+
'rho_max': {'type': 'number', 'minimum': 0.5, 'maximum': 2}
|
| 120 |
+
}
|
| 121 |
+
},
|
| 122 |
+
'default_parameters': {'nx': 100, 'nt': 200, 'length': 10.0, 'time': 2.0, 'u_max': 1.0, 'rho_max': 1.0}
|
| 123 |
+
},
|
| 124 |
+
{
|
| 125 |
+
'name': 'Phugoid - Trajectoire de Vol',
|
| 126 |
+
'slug': 'phugoid',
|
| 127 |
+
'description': 'Trajectoire de vol phugoid (mouvement oscillatoire d\'un planeur) - numerical-mooc.',
|
| 128 |
+
'theory': '''
|
| 129 |
+
## Phugoid
|
| 130 |
+
|
| 131 |
+
Mouvement ondulatoire d'un aircraft autour de sa trajectoire de trim.
|
| 132 |
+
|
| 133 |
+
Paramètres:
|
| 134 |
+
- zt: Hauteur de trim
|
| 135 |
+
- z0: Hauteur initiale
|
| 136 |
+
- θ0: Angle d'assiette initial
|
| 137 |
+
'''.strip(),
|
| 138 |
+
'parameters_schema': {
|
| 139 |
+
'type': 'object',
|
| 140 |
+
'properties': {
|
| 141 |
+
'zt': {'type': 'number', 'minimum': 0.1, 'maximum': 10},
|
| 142 |
+
'z0': {'type': 'number', 'minimum': 0.1, 'maximum': 20},
|
| 143 |
+
'theta0': {'type': 'number', 'minimum': -45, 'maximum': 45},
|
| 144 |
+
'N': {'type': 'integer', 'minimum': 100, 'maximum': 10000}
|
| 145 |
+
}
|
| 146 |
+
},
|
| 147 |
+
'default_parameters': {'zt': 1.0, 'z0': 2.0, 'theta0': 5.0, 'N': 1000}
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
'name': 'Elasticité - DangVan',
|
| 151 |
+
'slug': 'elasticity-dangvan',
|
| 152 |
+
'description': 'Calcul du tenseur deviatoire et critères de Tresca/Von Mises (DangVan).',
|
| 153 |
+
'theory': '''
|
| 154 |
+
## Tenseur Deviatoire
|
| 155 |
+
|
| 156 |
+
σ deviatorique = σ - (tr(σ)/3) × I
|
| 157 |
+
|
| 158 |
+
Critères:
|
| 159 |
+
- Tresca: max(σi) - min(σi)
|
| 160 |
+
- Von Mises: √(3/2 × Σ sij²)
|
| 161 |
+
'''.strip(),
|
| 162 |
+
'parameters_schema': {
|
| 163 |
+
'type': 'object',
|
| 164 |
+
'properties': {
|
| 165 |
+
'tensor': {'type': 'array', 'items': {'type': 'number'}, 'minItems': 9, 'maxItems': 9}
|
| 166 |
+
}
|
| 167 |
+
},
|
| 168 |
+
'default_parameters': {'tensor': [100, 0, 0, 0, 50, 0, 0, 0, 30]}
|
| 169 |
+
},
|
| 170 |
+
{
|
| 171 |
+
'name': 'Équation d\'Onde',
|
| 172 |
+
'slug': 'wave-equation',
|
| 173 |
+
'description': 'Résolution de l\'équation d\'onde 1D par différences finies.',
|
| 174 |
+
'theory': '''
|
| 175 |
+
## Équation d'Onde
|
| 176 |
+
|
| 177 |
+
∂²u/∂t² = c² × ∂²u/∂x²
|
| 178 |
+
|
| 179 |
+
Schéma explicite centré avec condition CFL: r = c×dt/dx ≤ 1
|
| 180 |
+
'''.strip(),
|
| 181 |
+
'parameters_schema': {
|
| 182 |
+
'type': 'object',
|
| 183 |
+
'properties': {
|
| 184 |
+
'length': {'type': 'number', 'minimum': 0.1, 'maximum': 100},
|
| 185 |
+
'time': {'type': 'number', 'minimum': 0.1, 'maximum': 10},
|
| 186 |
+
'nx': {'type': 'integer', 'minimum': 10, 'maximum': 500},
|
| 187 |
+
'nt': {'type': 'integer', 'minimum': 100, 'maximum': 5000},
|
| 188 |
+
'wave_speed': {'type': 'number', 'minimum': 0.1, 'maximum': 10}
|
| 189 |
+
}
|
| 190 |
+
},
|
| 191 |
+
'default_parameters': {'length': 1.0, 'time': 1.0, 'nx': 100, 'nt': 500, 'wave_speed': 1.0}
|
| 192 |
+
},
|
| 193 |
+
]
|
| 194 |
+
|
| 195 |
+
for method_data in methods:
|
| 196 |
+
method, created = SimulationMethod.objects.update_or_create(
|
| 197 |
+
slug=method_data['slug'],
|
| 198 |
+
defaults=method_data
|
| 199 |
+
)
|
| 200 |
+
if created:
|
| 201 |
+
self.stdout.write(self.style.SUCCESS(f'Créé: {method.name}'))
|
| 202 |
+
else:
|
| 203 |
+
self.stdout.write(self.style.WARNING(f'Mis à jour: {method.name}'))
|
| 204 |
+
|
| 205 |
+
self.stdout.write(self.style.SUCCESS(f'{len(methods)} méthodes initialisées'))
|
simulations/migrations/0001_initial.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Generated by Django 6.0.1 on 2026-01-19 00:16
|
| 2 |
+
|
| 3 |
+
import django.db.models.deletion
|
| 4 |
+
from django.conf import settings
|
| 5 |
+
from django.db import migrations, models
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Migration(migrations.Migration):
|
| 9 |
+
|
| 10 |
+
initial = True
|
| 11 |
+
|
| 12 |
+
dependencies = [
|
| 13 |
+
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
| 14 |
+
]
|
| 15 |
+
|
| 16 |
+
operations = [
|
| 17 |
+
migrations.CreateModel(
|
| 18 |
+
name='SimulationMethod',
|
| 19 |
+
fields=[
|
| 20 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 21 |
+
('name', models.CharField(max_length=200)),
|
| 22 |
+
('slug', models.SlugField(unique=True)),
|
| 23 |
+
('description', models.TextField()),
|
| 24 |
+
('theory', models.TextField(blank=True)),
|
| 25 |
+
('parameters_schema', models.JSONField(default=dict)),
|
| 26 |
+
('default_parameters', models.JSONField(default=dict)),
|
| 27 |
+
('is_active', models.BooleanField(default=True)),
|
| 28 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 29 |
+
('updated_at', models.DateTimeField(auto_now=True)),
|
| 30 |
+
],
|
| 31 |
+
options={
|
| 32 |
+
'ordering': ['name'],
|
| 33 |
+
},
|
| 34 |
+
),
|
| 35 |
+
migrations.CreateModel(
|
| 36 |
+
name='SimulationRun',
|
| 37 |
+
fields=[
|
| 38 |
+
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
| 39 |
+
('name', models.CharField(blank=True, max_length=200)),
|
| 40 |
+
('parameters', models.JSONField(default=dict)),
|
| 41 |
+
('status', models.CharField(choices=[('PENDING', 'En attente'), ('RUNNING', 'En cours'), ('SUCCESS', 'Terminé'), ('FAILURE', 'Échoué'), ('CANCELLED', 'Annulé')], default='PENDING', max_length=20)),
|
| 42 |
+
('progress', models.IntegerField(default=0)),
|
| 43 |
+
('logs', models.TextField(blank=True)),
|
| 44 |
+
('result_data', models.JSONField(blank=True, null=True)),
|
| 45 |
+
('error_message', models.TextField(blank=True)),
|
| 46 |
+
('input_file', models.FileField(blank=True, null=True, upload_to='simulations/inputs/')),
|
| 47 |
+
('output_file', models.FileField(blank=True, null=True, upload_to='simulations/outputs/')),
|
| 48 |
+
('plot_file', models.FileField(blank=True, null=True, upload_to='simulations/plots/')),
|
| 49 |
+
('pdf_file', models.FileField(blank=True, null=True, upload_to='simulations/reports/')),
|
| 50 |
+
('csv_file', models.FileField(blank=True, null=True, upload_to='simulations/csv/')),
|
| 51 |
+
('created_at', models.DateTimeField(auto_now_add=True)),
|
| 52 |
+
('started_at', models.DateTimeField(blank=True, null=True)),
|
| 53 |
+
('completed_at', models.DateTimeField(blank=True, null=True)),
|
| 54 |
+
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
| 55 |
+
('method', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='runs', to='simulations.simulationmethod')),
|
| 56 |
+
],
|
| 57 |
+
options={
|
| 58 |
+
'ordering': ['-created_at'],
|
| 59 |
+
},
|
| 60 |
+
),
|
| 61 |
+
]
|
simulations/migrations/__init__.py
ADDED
|
File without changes
|
simulations/models.py
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
import csv
|
| 3 |
+
import io
|
| 4 |
+
from datetime import datetime
|
| 5 |
+
import numpy as np
|
| 6 |
+
import matplotlib
|
| 7 |
+
matplotlib.use('Agg')
|
| 8 |
+
import matplotlib.pyplot as plt
|
| 9 |
+
from django.db import models
|
| 10 |
+
from django.conf import settings
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class SimulationMethod(models.Model):
|
| 14 |
+
name = models.CharField(max_length=200)
|
| 15 |
+
slug = models.SlugField(unique=True)
|
| 16 |
+
description = models.TextField()
|
| 17 |
+
theory = models.TextField(blank=True)
|
| 18 |
+
parameters_schema = models.JSONField(default=dict)
|
| 19 |
+
default_parameters = models.JSONField(default=dict)
|
| 20 |
+
is_active = models.BooleanField(default=True)
|
| 21 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 22 |
+
updated_at = models.DateTimeField(auto_now=True)
|
| 23 |
+
|
| 24 |
+
class Meta:
|
| 25 |
+
ordering = ['name']
|
| 26 |
+
|
| 27 |
+
def __str__(self):
|
| 28 |
+
return self.name
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class SimulationRun(models.Model):
|
| 32 |
+
STATUS_CHOICES = [
|
| 33 |
+
('PENDING', 'En attente'),
|
| 34 |
+
('RUNNING', 'En cours'),
|
| 35 |
+
('SUCCESS', 'Terminé'),
|
| 36 |
+
('FAILURE', 'Échoué'),
|
| 37 |
+
('CANCELLED', 'Annulé'),
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
method = models.ForeignKey(
|
| 41 |
+
SimulationMethod,
|
| 42 |
+
on_delete=models.CASCADE,
|
| 43 |
+
related_name='runs'
|
| 44 |
+
)
|
| 45 |
+
name = models.CharField(max_length=200, blank=True)
|
| 46 |
+
parameters = models.JSONField(default=dict)
|
| 47 |
+
status = models.CharField(
|
| 48 |
+
max_length=20,
|
| 49 |
+
choices=STATUS_CHOICES,
|
| 50 |
+
default='PENDING'
|
| 51 |
+
)
|
| 52 |
+
progress = models.IntegerField(default=0)
|
| 53 |
+
logs = models.TextField(blank=True)
|
| 54 |
+
result_data = models.JSONField(null=True, blank=True)
|
| 55 |
+
error_message = models.TextField(blank=True)
|
| 56 |
+
input_file = models.FileField(
|
| 57 |
+
upload_to='simulations/inputs/',
|
| 58 |
+
null=True,
|
| 59 |
+
blank=True
|
| 60 |
+
)
|
| 61 |
+
output_file = models.FileField(
|
| 62 |
+
upload_to='simulations/outputs/',
|
| 63 |
+
null=True,
|
| 64 |
+
blank=True
|
| 65 |
+
)
|
| 66 |
+
plot_file = models.FileField(
|
| 67 |
+
upload_to='simulations/plots/',
|
| 68 |
+
null=True,
|
| 69 |
+
blank=True
|
| 70 |
+
)
|
| 71 |
+
pdf_file = models.FileField(
|
| 72 |
+
upload_to='simulations/reports/',
|
| 73 |
+
null=True,
|
| 74 |
+
blank=True
|
| 75 |
+
)
|
| 76 |
+
csv_file = models.FileField(
|
| 77 |
+
upload_to='simulations/csv/',
|
| 78 |
+
null=True,
|
| 79 |
+
blank=True
|
| 80 |
+
)
|
| 81 |
+
created_by = models.ForeignKey(
|
| 82 |
+
settings.AUTH_USER_MODEL,
|
| 83 |
+
on_delete=models.SET_NULL,
|
| 84 |
+
null=True,
|
| 85 |
+
blank=True
|
| 86 |
+
)
|
| 87 |
+
created_at = models.DateTimeField(auto_now_add=True)
|
| 88 |
+
started_at = models.DateTimeField(null=True, blank=True)
|
| 89 |
+
completed_at = models.DateTimeField(null=True, blank=True)
|
| 90 |
+
|
| 91 |
+
class Meta:
|
| 92 |
+
ordering = ['-created_at']
|
| 93 |
+
|
| 94 |
+
def __str__(self):
|
| 95 |
+
return self.name or f"Run #{self.id} - {self.method.name}"
|
| 96 |
+
|
| 97 |
+
def add_log(self, message):
|
| 98 |
+
from django.utils import timezone
|
| 99 |
+
timestamp = timezone.now().strftime('%Y-%m-%d %H:%M:%S')
|
| 100 |
+
self.logs += f"[{timestamp}] {message}\n"
|
| 101 |
+
self.save(update_fields=['logs'])
|
| 102 |
+
|
| 103 |
+
def set_running(self):
|
| 104 |
+
from django.utils import timezone
|
| 105 |
+
self.status = 'RUNNING'
|
| 106 |
+
self.started_at = timezone.now()
|
| 107 |
+
self.save(update_fields=['status', 'started_at'])
|
| 108 |
+
|
| 109 |
+
def set_success(self, result_data=None):
|
| 110 |
+
from django.utils import timezone
|
| 111 |
+
self.status = 'SUCCESS'
|
| 112 |
+
self.progress = 100
|
| 113 |
+
self.completed_at = timezone.now()
|
| 114 |
+
if result_data:
|
| 115 |
+
self.result_data = result_data
|
| 116 |
+
self.save(update_fields=['status', 'progress', 'completed_at', 'result_data'])
|
| 117 |
+
self.generate_outputs()
|
| 118 |
+
|
| 119 |
+
def set_failure(self, error_message):
|
| 120 |
+
from django.utils import timezone
|
| 121 |
+
self.status = 'FAILURE'
|
| 122 |
+
self.completed_at = timezone.now()
|
| 123 |
+
self.error_message = error_message
|
| 124 |
+
self.save(update_fields=['status', 'completed_at', 'error_message'])
|
| 125 |
+
|
| 126 |
+
def set_pending(self):
|
| 127 |
+
self.status = 'PENDING'
|
| 128 |
+
self.progress = 0
|
| 129 |
+
self.started_at = None
|
| 130 |
+
self.completed_at = None
|
| 131 |
+
self.save(update_fields=['status', 'progress', 'started_at', 'completed_at'])
|
| 132 |
+
|
| 133 |
+
def generate_plot(self):
|
| 134 |
+
"""Génère un graphique selon la méthode."""
|
| 135 |
+
if not self.result_data:
|
| 136 |
+
return None
|
| 137 |
+
|
| 138 |
+
fig, ax = plt.subplots(figsize=(10, 6))
|
| 139 |
+
|
| 140 |
+
method_slug = self.method.slug
|
| 141 |
+
|
| 142 |
+
if method_slug == 'monte-carlo-pi':
|
| 143 |
+
self._plot_monte_carlo(fig, ax)
|
| 144 |
+
elif method_slug == 'diffusion-1d':
|
| 145 |
+
self._plot_diffusion(fig, ax)
|
| 146 |
+
elif method_slug == 'linear-solve':
|
| 147 |
+
self._plot_linear(fig, ax)
|
| 148 |
+
elif method_slug == 'heat-conduction':
|
| 149 |
+
self._plot_heat_conduction(fig, ax)
|
| 150 |
+
elif method_slug == 'traffic-flow':
|
| 151 |
+
self._plot_traffic_flow(fig, ax)
|
| 152 |
+
elif method_slug == 'phugoid':
|
| 153 |
+
self._plot_phugoid(fig, ax)
|
| 154 |
+
elif method_slug == 'elasticity-dangvan':
|
| 155 |
+
self._plot_elasticity(fig, ax)
|
| 156 |
+
elif method_slug == 'wave-equation':
|
| 157 |
+
self._plot_wave_equation(fig, ax)
|
| 158 |
+
else:
|
| 159 |
+
ax.text(0.5, 0.5, 'Pas de visualisation disponible',
|
| 160 |
+
ha='center', va='center', transform=ax.transAxes)
|
| 161 |
+
|
| 162 |
+
plt.tight_layout()
|
| 163 |
+
|
| 164 |
+
buffer = io.BytesIO()
|
| 165 |
+
plt.savefig(buffer, format='png', dpi=150)
|
| 166 |
+
buffer.seek(0)
|
| 167 |
+
plt.close(fig)
|
| 168 |
+
|
| 169 |
+
return buffer
|
| 170 |
+
|
| 171 |
+
def _plot_monte_carlo(self, fig, ax):
|
| 172 |
+
"""Graphique pour Monte Carlo."""
|
| 173 |
+
data = self.result_data
|
| 174 |
+
ax.bar(['Estimé', 'Exact'], [data['pi_estimate'], data['exact_pi']],
|
| 175 |
+
color=['#3498db', '#27ae60'])
|
| 176 |
+
ax.set_ylabel('Valeur de Pi')
|
| 177 |
+
ax.set_title(f"Estimation de Pi - {data['n_points']} points")
|
| 178 |
+
ax.set_ylim(0, 4)
|
| 179 |
+
error_pct = abs(data['error']) / data['exact_pi'] * 100
|
| 180 |
+
ax.text(0.5, 0.9, f"Erreur: {error_pct:.4f}%",
|
| 181 |
+
transform=ax.transAxes, ha='center')
|
| 182 |
+
|
| 183 |
+
def _plot_diffusion(self, fig, ax):
|
| 184 |
+
"""Graphique pour diffusion 1D."""
|
| 185 |
+
data = self.result_data
|
| 186 |
+
final_state = data.get('final_state', [])
|
| 187 |
+
if final_state:
|
| 188 |
+
x = np.linspace(0, len(final_state) - 1, len(final_state))
|
| 189 |
+
ax.plot(x, final_state, 'b-', linewidth=2, label='État final')
|
| 190 |
+
ax.set_xlabel('Position x')
|
| 191 |
+
ax.set_ylabel('Concentration u(x,t)')
|
| 192 |
+
ax.set_title('Profil de diffusion 1D - État final')
|
| 193 |
+
ax.grid(True, alpha=0.3)
|
| 194 |
+
ax.legend()
|
| 195 |
+
ax.fill_between(x, final_state, alpha=0.3)
|
| 196 |
+
ax.set_xlim(0, len(final_state) if final_state else 100)
|
| 197 |
+
|
| 198 |
+
def _plot_linear(self, fig, ax):
|
| 199 |
+
"""Graphique pour résolution linéaire."""
|
| 200 |
+
data = self.result_data
|
| 201 |
+
info = f"Taille: {data['size']}\n"
|
| 202 |
+
info += f"Norme solution: {data['solution_norm']:.4f}\n"
|
| 203 |
+
info += f"Résidu: {data['residual']:.2e}\n"
|
| 204 |
+
info += f"Nombre de condition: {data['condition_number']:.2e}"
|
| 205 |
+
ax.text(0.5, 0.5, info, transform=ax.transAxes, ha='center', va='center',
|
| 206 |
+
fontsize=12, bbox=dict(boxstyle='round', facecolor='wheat', alpha=0.5))
|
| 207 |
+
ax.set_title('Résultats - Résolution système linéaire')
|
| 208 |
+
|
| 209 |
+
def _plot_heat_conduction(self, fig, ax):
|
| 210 |
+
"""Graphique pour conduction thermique."""
|
| 211 |
+
data = self.result_data
|
| 212 |
+
T_final = np.array(data.get('final_T', []))
|
| 213 |
+
T_analytical = np.array(data.get('T_analytical', []))
|
| 214 |
+
N = len(T_final)
|
| 215 |
+
t = np.arange(N) * data.get('dt', 0.001)
|
| 216 |
+
|
| 217 |
+
ax.plot(t, T_final, 'b-', linewidth=2, label='Numérique')
|
| 218 |
+
ax.plot(t, T_analytical, 'r--', linewidth=2, label='Analytique')
|
| 219 |
+
ax.set_xlabel('Temps')
|
| 220 |
+
ax.set_ylabel('Température')
|
| 221 |
+
ax.set_title(f"Conduction thermique - θ={data.get('theta', 0.5)}")
|
| 222 |
+
ax.legend()
|
| 223 |
+
ax.grid(True, alpha=0.3)
|
| 224 |
+
|
| 225 |
+
def _plot_traffic_flow(self, fig, ax):
|
| 226 |
+
"""Graphique pour flux de trafic."""
|
| 227 |
+
data = self.result_data
|
| 228 |
+
rho_matrix = np.array(data.get('density_matrix', []))
|
| 229 |
+
if rho_matrix.size > 0:
|
| 230 |
+
im = ax.imshow(rho_matrix, aspect='auto', cmap='hot', origin='lower')
|
| 231 |
+
ax.set_xlabel('Position x')
|
| 232 |
+
ax.set_ylabel('Temps t')
|
| 233 |
+
ax.set_title('Densité de trafic')
|
| 234 |
+
plt.colorbar(im, ax=ax, label='Densité')
|
| 235 |
+
else:
|
| 236 |
+
ax.text(0.5, 0.5, 'Pas de données', ha='center', va='center')
|
| 237 |
+
|
| 238 |
+
def _plot_phugoid(self, fig, ax):
|
| 239 |
+
"""Graphique pour trajectoire phugoid."""
|
| 240 |
+
data = self.result_data
|
| 241 |
+
x = np.array(data.get('x', []))
|
| 242 |
+
z = np.array(data.get('z', []))
|
| 243 |
+
if x.size > 0:
|
| 244 |
+
ax.plot(x, -z, 'b-', linewidth=2)
|
| 245 |
+
ax.set_xlabel('x')
|
| 246 |
+
ax.set_ylabel('z')
|
| 247 |
+
ax.set_title(f"Trajectoire de vol - C={data.get('C', 0):.3f}")
|
| 248 |
+
ax.grid(True, alpha=0.3)
|
| 249 |
+
ax.set_aspect('equal')
|
| 250 |
+
|
| 251 |
+
def _plot_elasticity(self, fig, ax):
|
| 252 |
+
"""Graphique pour élasticité DangVan."""
|
| 253 |
+
data = self.result_data
|
| 254 |
+
tensor = np.array(data.get('tensor_matrix', [])).reshape(3, 3)
|
| 255 |
+
im = ax.imshow(tensor, cmap='coolwarm', aspect='equal')
|
| 256 |
+
ax.set_title('Tenseur des contraintes (MPa)')
|
| 257 |
+
for i in range(3):
|
| 258 |
+
for j in range(3):
|
| 259 |
+
ax.text(j, i, f'{tensor[i,j]:.1f}', ha='center', va='center', color='black')
|
| 260 |
+
plt.colorbar(im, ax=ax)
|
| 261 |
+
|
| 262 |
+
def _plot_wave_equation(self, fig, ax):
|
| 263 |
+
"""Graphique pour équation d'onde."""
|
| 264 |
+
data = self.result_data
|
| 265 |
+
u = np.array(data.get('displacement_matrix', []))
|
| 266 |
+
if u.size > 0:
|
| 267 |
+
im = ax.imshow(u, aspect='auto', cmap='RdBu', origin='lower')
|
| 268 |
+
ax.set_xlabel('Position x')
|
| 269 |
+
ax.set_ylabel('Temps t')
|
| 270 |
+
ax.set_title('Déplacement u(x,t)')
|
| 271 |
+
plt.colorbar(im, ax=ax, label='u')
|
| 272 |
+
|
| 273 |
+
def generate_csv(self):
|
| 274 |
+
"""Génère un fichier CSV des résultats."""
|
| 275 |
+
if not self.result_data:
|
| 276 |
+
return None
|
| 277 |
+
|
| 278 |
+
buffer = io.StringIO()
|
| 279 |
+
writer = csv.writer(buffer)
|
| 280 |
+
|
| 281 |
+
writer.writerow(['Simulation Run Report'])
|
| 282 |
+
writer.writerow(['ID', self.id])
|
| 283 |
+
writer.writerow(['Méthode', self.method.name])
|
| 284 |
+
writer.writerow(['Statut', self.status])
|
| 285 |
+
writer.writerow(['Créé le', self.created_at])
|
| 286 |
+
writer.writerow(['Terminé le', self.completed_at])
|
| 287 |
+
writer.writerow([])
|
| 288 |
+
|
| 289 |
+
writer.writerow(['Paramètres'])
|
| 290 |
+
for key, value in self.parameters.items():
|
| 291 |
+
writer.writerow([key, str(value)])
|
| 292 |
+
writer.writerow([])
|
| 293 |
+
|
| 294 |
+
writer.writerow(['Résultats'])
|
| 295 |
+
for key, value in self.result_data.items():
|
| 296 |
+
writer.writerow([key, str(value)])
|
| 297 |
+
|
| 298 |
+
buffer.seek(0)
|
| 299 |
+
return io.BytesIO(buffer.getvalue().encode('utf-8'))
|
| 300 |
+
|
| 301 |
+
def generate_pdf(self):
|
| 302 |
+
"""Génère un rapport PDF avec ReportLab."""
|
| 303 |
+
from reportlab.lib.pagesizes import A4
|
| 304 |
+
from reportlab.lib import colors
|
| 305 |
+
from reportlab.lib.units import inch
|
| 306 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
|
| 307 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 308 |
+
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
| 309 |
+
import tempfile
|
| 310 |
+
|
| 311 |
+
buffer = io.BytesIO()
|
| 312 |
+
doc = SimpleDocTemplate(buffer, pagesize=A4,
|
| 313 |
+
leftMargin=0.5*inch, rightMargin=0.5*inch,
|
| 314 |
+
topMargin=0.5*inch, bottomMargin=0.5*inch)
|
| 315 |
+
|
| 316 |
+
styles = getSampleStyleSheet()
|
| 317 |
+
title_style = ParagraphStyle('Title', parent=styles['Heading1'],
|
| 318 |
+
fontSize=18, spaceAfter=20, alignment=TA_CENTER)
|
| 319 |
+
heading_style = ParagraphStyle('Heading', parent=styles['Heading2'],
|
| 320 |
+
fontSize=14, spaceAfter=10, color=colors.darkblue)
|
| 321 |
+
normal_style = styles['Normal']
|
| 322 |
+
|
| 323 |
+
story = []
|
| 324 |
+
|
| 325 |
+
story.append(Paragraph(f"Rapport de Simulation: {self.method.name}", title_style))
|
| 326 |
+
story.append(Spacer(1, 20))
|
| 327 |
+
|
| 328 |
+
story.append(Paragraph("Informations Générales", heading_style))
|
| 329 |
+
info_data = [
|
| 330 |
+
['ID de la simulation', str(self.id)],
|
| 331 |
+
['Méthode', self.method.name],
|
| 332 |
+
['Statut', self.status],
|
| 333 |
+
['Créé le', str(self.created_at)],
|
| 334 |
+
['Terminé le', str(self.completed_at)],
|
| 335 |
+
]
|
| 336 |
+
info_table = Table(info_data, colWidths=[2.5*inch, 3*inch])
|
| 337 |
+
info_table.setStyle(TableStyle([
|
| 338 |
+
('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
|
| 339 |
+
('TEXTCOLOR', (0, 0), (0, -1), colors.black),
|
| 340 |
+
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
| 341 |
+
('PADDING', (0, 0), (-1, -1), 6),
|
| 342 |
+
]))
|
| 343 |
+
story.append(info_table)
|
| 344 |
+
story.append(Spacer(1, 20))
|
| 345 |
+
|
| 346 |
+
story.append(Paragraph("Paramètres de Simulation", heading_style))
|
| 347 |
+
params_data = [[str(k), str(v)] for k, v in self.parameters.items()]
|
| 348 |
+
if params_data:
|
| 349 |
+
params_table = Table(params_data, colWidths=[2.5*inch, 3*inch])
|
| 350 |
+
params_table.setStyle(TableStyle([
|
| 351 |
+
('BACKGROUND', (0, 0), (0, -1), colors.lightblue),
|
| 352 |
+
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
| 353 |
+
('PADDING', (0, 0), (-1, -1), 6),
|
| 354 |
+
]))
|
| 355 |
+
story.append(params_table)
|
| 356 |
+
else:
|
| 357 |
+
story.append(Paragraph("Aucun paramètre personnalisé", normal_style))
|
| 358 |
+
story.append(Spacer(1, 20))
|
| 359 |
+
|
| 360 |
+
story.append(Paragraph("Résultats", heading_style))
|
| 361 |
+
results_data = [[str(k), str(v)] for k, v in self.result_data.items()]
|
| 362 |
+
if results_data:
|
| 363 |
+
results_table = Table(results_data, colWidths=[2.5*inch, 3*inch])
|
| 364 |
+
results_table.setStyle(TableStyle([
|
| 365 |
+
('BACKGROUND', (0, 0), (0, -1), colors.lightgreen),
|
| 366 |
+
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
| 367 |
+
('PADDING', (0, 0), (-1, -1), 6),
|
| 368 |
+
]))
|
| 369 |
+
story.append(results_table)
|
| 370 |
+
story.append(Spacer(1, 20))
|
| 371 |
+
|
| 372 |
+
story.append(Paragraph("Graphique", heading_style))
|
| 373 |
+
import os
|
| 374 |
+
if self.plot_file and os.path.exists(self.plot_file.path):
|
| 375 |
+
img = Image(self.plot_file.path, width=5*inch, height=3*inch)
|
| 376 |
+
img.hAlign = 'CENTER'
|
| 377 |
+
story.append(img)
|
| 378 |
+
else:
|
| 379 |
+
story.append(Paragraph("Graphique non disponible", normal_style))
|
| 380 |
+
story.append(Spacer(1, 20))
|
| 381 |
+
|
| 382 |
+
if self.logs:
|
| 383 |
+
story.append(Paragraph("Logs d'Éxecution", heading_style))
|
| 384 |
+
log_style = ParagraphStyle('Log', parent=styles['Normal'],
|
| 385 |
+
fontSize=8, textColor=colors.darkgrey)
|
| 386 |
+
for line in self.logs.strip().split('\n')[-50:]:
|
| 387 |
+
story.append(Paragraph(line, log_style))
|
| 388 |
+
|
| 389 |
+
story.append(Spacer(1, 30))
|
| 390 |
+
story.append(Paragraph(f"Généré le {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
| 391 |
+
ParagraphStyle('Date', parent=styles['Normal'],
|
| 392 |
+
fontSize=8, alignment=TA_CENTER)))
|
| 393 |
+
|
| 394 |
+
doc.build(story)
|
| 395 |
+
|
| 396 |
+
buffer.seek(0)
|
| 397 |
+
return buffer
|
| 398 |
+
|
| 399 |
+
def generate_outputs(self):
|
| 400 |
+
"""Génère tous les fichiers de sortie."""
|
| 401 |
+
import os
|
| 402 |
+
|
| 403 |
+
plot_buffer = self.generate_plot()
|
| 404 |
+
if plot_buffer:
|
| 405 |
+
filename = f"plot_{self.id}.png"
|
| 406 |
+
self.plot_file.save(filename, plot_buffer, save=True)
|
| 407 |
+
|
| 408 |
+
csv_buffer = self.generate_csv()
|
| 409 |
+
if csv_buffer:
|
| 410 |
+
filename = f"results_{self.id}.csv"
|
| 411 |
+
self.csv_file.save(filename, csv_buffer, save=True)
|
| 412 |
+
|
| 413 |
+
if self.plot_file:
|
| 414 |
+
pdf_buffer = self.generate_pdf_with_plot()
|
| 415 |
+
if pdf_buffer:
|
| 416 |
+
filename = f"report_{self.id}.pdf"
|
| 417 |
+
self.pdf_file.save(filename, pdf_buffer, save=True)
|
| 418 |
+
|
| 419 |
+
def generate_pdf_with_plot(self):
|
| 420 |
+
"""Génère un rapport PDF avec le graphique déjà sauvegardé."""
|
| 421 |
+
from reportlab.lib.pagesizes import A4
|
| 422 |
+
from reportlab.lib import colors
|
| 423 |
+
from reportlab.lib.units import inch
|
| 424 |
+
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, Image
|
| 425 |
+
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
|
| 426 |
+
from reportlab.lib.enums import TA_CENTER, TA_LEFT
|
| 427 |
+
import os
|
| 428 |
+
|
| 429 |
+
if not self.plot_file or not os.path.exists(self.plot_file.path):
|
| 430 |
+
return None
|
| 431 |
+
|
| 432 |
+
buffer = io.BytesIO()
|
| 433 |
+
doc = SimpleDocTemplate(buffer, pagesize=A4,
|
| 434 |
+
leftMargin=0.5*inch, rightMargin=0.5*inch,
|
| 435 |
+
topMargin=0.5*inch, bottomMargin=0.5*inch)
|
| 436 |
+
|
| 437 |
+
styles = getSampleStyleSheet()
|
| 438 |
+
title_style = ParagraphStyle('Title', parent=styles['Heading1'],
|
| 439 |
+
fontSize=18, spaceAfter=20, alignment=TA_CENTER)
|
| 440 |
+
heading_style = ParagraphStyle('Heading', parent=styles['Heading2'],
|
| 441 |
+
fontSize=14, spaceAfter=10, color=colors.darkblue)
|
| 442 |
+
normal_style = styles['Normal']
|
| 443 |
+
|
| 444 |
+
story = []
|
| 445 |
+
|
| 446 |
+
story.append(Paragraph(f"Rapport de Simulation: {self.method.name}", title_style))
|
| 447 |
+
story.append(Spacer(1, 20))
|
| 448 |
+
|
| 449 |
+
story.append(Paragraph("Informations Générales", heading_style))
|
| 450 |
+
info_data = [
|
| 451 |
+
['ID de la simulation', str(self.id)],
|
| 452 |
+
['Méthode', self.method.name],
|
| 453 |
+
['Statut', self.status],
|
| 454 |
+
['Créé le', str(self.created_at)],
|
| 455 |
+
['Terminé le', str(self.completed_at)],
|
| 456 |
+
]
|
| 457 |
+
info_table = Table(info_data, colWidths=[2.5*inch, 3*inch])
|
| 458 |
+
info_table.setStyle(TableStyle([
|
| 459 |
+
('BACKGROUND', (0, 0), (0, -1), colors.lightgrey),
|
| 460 |
+
('TEXTCOLOR', (0, 0), (0, -1), colors.black),
|
| 461 |
+
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
| 462 |
+
('PADDING', (0, 0), (-1, -1), 6),
|
| 463 |
+
]))
|
| 464 |
+
story.append(info_table)
|
| 465 |
+
story.append(Spacer(1, 20))
|
| 466 |
+
|
| 467 |
+
story.append(Paragraph("Paramètres de Simulation", heading_style))
|
| 468 |
+
params_data = [[str(k), str(v)] for k, v in self.parameters.items()]
|
| 469 |
+
if params_data:
|
| 470 |
+
params_table = Table(params_data, colWidths=[2.5*inch, 3*inch])
|
| 471 |
+
params_table.setStyle(TableStyle([
|
| 472 |
+
('BACKGROUND', (0, 0), (0, -1), colors.lightblue),
|
| 473 |
+
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
| 474 |
+
('PADDING', (0, 0), (-1, -1), 6),
|
| 475 |
+
]))
|
| 476 |
+
story.append(params_table)
|
| 477 |
+
else:
|
| 478 |
+
story.append(Paragraph("Aucun paramètre personnalisé", normal_style))
|
| 479 |
+
story.append(Spacer(1, 20))
|
| 480 |
+
|
| 481 |
+
story.append(Paragraph("Résultats", heading_style))
|
| 482 |
+
if self.result_data:
|
| 483 |
+
results_data = [[str(k), str(v)] for k, v in self.result_data.items()]
|
| 484 |
+
if results_data:
|
| 485 |
+
results_table = Table(results_data, colWidths=[2.5*inch, 3*inch])
|
| 486 |
+
results_table.setStyle(TableStyle([
|
| 487 |
+
('BACKGROUND', (0, 0), (0, -1), colors.lightgreen),
|
| 488 |
+
('GRID', (0, 0), (-1, -1), 0.5, colors.grey),
|
| 489 |
+
('PADDING', (0, 0), (-1, -1), 6),
|
| 490 |
+
]))
|
| 491 |
+
story.append(results_table)
|
| 492 |
+
story.append(Spacer(1, 20))
|
| 493 |
+
|
| 494 |
+
story.append(Paragraph("Graphique", heading_style))
|
| 495 |
+
img = Image(self.plot_file.path, width=5*inch, height=3*inch)
|
| 496 |
+
img.hAlign = 'CENTER'
|
| 497 |
+
story.append(img)
|
| 498 |
+
story.append(Spacer(1, 20))
|
| 499 |
+
|
| 500 |
+
if self.logs:
|
| 501 |
+
story.append(Paragraph("Logs d'Éxecution", heading_style))
|
| 502 |
+
log_style = ParagraphStyle('Log', parent=styles['Normal'],
|
| 503 |
+
fontSize=8, textColor=colors.darkgrey)
|
| 504 |
+
for line in self.logs.strip().split('\n')[-50:]:
|
| 505 |
+
story.append(Paragraph(line, log_style))
|
| 506 |
+
|
| 507 |
+
story.append(Spacer(1, 30))
|
| 508 |
+
story.append(Paragraph(f"Généré le {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
|
| 509 |
+
ParagraphStyle('Date', parent=styles['Normal'],
|
| 510 |
+
fontSize=8, alignment=TA_CENTER)))
|
| 511 |
+
|
| 512 |
+
doc.build(story)
|
| 513 |
+
|
| 514 |
+
buffer.seek(0)
|
| 515 |
+
return buffer
|
simulations/registry.py
ADDED
|
@@ -0,0 +1,181 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Système extensible pour les méthodes de simulation numérique.
|
| 3 |
+
|
| 4 |
+
Pour ajouter une nouvelle méthode :
|
| 5 |
+
1. Créer une fonction dans tasks.py qui retourne un générateur
|
| 6 |
+
2. Ajouter la méthode au SIMULATION_METHODS dict dans tasks.py
|
| 7 |
+
3. Créer un SimulationMethod via l'admin ou init_simulation_methods
|
| 8 |
+
|
| 9 |
+
Le système gérera automatiquement :
|
| 10 |
+
- L'affichage des résultats
|
| 11 |
+
- La génération des fichiers (PNG, PDF, CSV)
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
from simulations.models import SimulationMethod, SimulationRun
|
| 15 |
+
from simulations.tasks import SIMULATION_METHODS
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def get_method_info(slug):
|
| 19 |
+
"""Récupère les informations sur une méthode de simulation."""
|
| 20 |
+
try:
|
| 21 |
+
method = SimulationMethod.objects.get(slug=slug)
|
| 22 |
+
return {
|
| 23 |
+
'name': method.name,
|
| 24 |
+
'slug': method.slug,
|
| 25 |
+
'description': method.description,
|
| 26 |
+
'theory': method.theory,
|
| 27 |
+
'default_parameters': method.default_parameters,
|
| 28 |
+
}
|
| 29 |
+
except SimulationMethod.DoesNotExist:
|
| 30 |
+
return None
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def get_all_methods():
|
| 34 |
+
"""Liste toutes les méthodes disponibles."""
|
| 35 |
+
return list(SIMULATION_METHODS.keys())
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
def method_exists(slug):
|
| 39 |
+
"""Vérifie si une méthode existe."""
|
| 40 |
+
return slug in SIMULATION_METHODS
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def get_method_description(slug):
|
| 44 |
+
"""Retourne une description lisible pour une méthode."""
|
| 45 |
+
descriptions = {
|
| 46 |
+
'monte-carlo-pi': 'Estimation de π par la méthode de Monte Carlo',
|
| 47 |
+
'diffusion-1d': 'Résolution de l\'équation de diffusion 1D',
|
| 48 |
+
'linear-solve': 'Résolution de systèmes linéaires Ax = b',
|
| 49 |
+
'heat-conduction': 'Simulation de conduction thermique',
|
| 50 |
+
'traffic-flow': 'Modèle de trafic 1D (Lighthill-Whitham-Richards)',
|
| 51 |
+
'phugoid': 'Trajectoire de vol phugoid (mouvement oscillatoire)',
|
| 52 |
+
'elasticity-dangvan': 'Calcul du tenseur deviatoire et critères de plasticité',
|
| 53 |
+
'wave-equation': 'Résolution de l\'équation d\'onde 1D',
|
| 54 |
+
}
|
| 55 |
+
return descriptions.get(slug, 'Méthode de simulation numérique')
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def format_result_value(value):
|
| 59 |
+
"""Formate une valeur de résultat pour l'affichage."""
|
| 60 |
+
if isinstance(value, (int, float)):
|
| 61 |
+
if abs(value) < 0.001 or abs(value) > 10000:
|
| 62 |
+
return f"{value:.2e}"
|
| 63 |
+
return f"{value:.4f}"
|
| 64 |
+
elif isinstance(value, list):
|
| 65 |
+
if len(value) <= 5:
|
| 66 |
+
return ', '.join(format_result_value(v) for v in value)
|
| 67 |
+
else:
|
| 68 |
+
return f"[{len(value)} éléments]"
|
| 69 |
+
elif isinstance(value, dict):
|
| 70 |
+
return f"{{ {len(value)} clés }}"
|
| 71 |
+
return str(value)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def get_result_summary(result_data):
|
| 75 |
+
"""Génère un résumé des résultats."""
|
| 76 |
+
if not result_data:
|
| 77 |
+
return "Aucun résultat"
|
| 78 |
+
|
| 79 |
+
summary = []
|
| 80 |
+
for key, value in result_data.items():
|
| 81 |
+
if key in ['tensor_matrix', 'deviator_tensor', 'displacement_matrix',
|
| 82 |
+
'density_matrix', 'final_T', 'x', 'z']:
|
| 83 |
+
continue
|
| 84 |
+
formatted = format_result_value(value)
|
| 85 |
+
summary.append(f"{key}: {formatted}")
|
| 86 |
+
|
| 87 |
+
return ' | '.join(summary[:5]) if summary else "Résultats disponibles"
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
# Registre des configurations d'affichage par méthode
|
| 91 |
+
DISPLAY_CONFIG = {
|
| 92 |
+
'monte-carlo-pi': {
|
| 93 |
+
'icon': '🎲',
|
| 94 |
+
'title': 'Estimation de Pi',
|
| 95 |
+
'metrics': ['pi_estimate', 'exact_pi', 'error', 'n_points'],
|
| 96 |
+
},
|
| 97 |
+
'diffusion-1d': {
|
| 98 |
+
'icon': '🌡️',
|
| 99 |
+
'title': 'Diffusion 1D',
|
| 100 |
+
'metrics': ['dx', 'dt', 'nx', 'nt', 'max_value'],
|
| 101 |
+
},
|
| 102 |
+
'linear-solve': {
|
| 103 |
+
'icon': '📐',
|
| 104 |
+
'title': 'Système Linéaire',
|
| 105 |
+
'metrics': ['size', 'solution_norm', 'residual', 'condition_number'],
|
| 106 |
+
},
|
| 107 |
+
'heat-conduction': {
|
| 108 |
+
'icon': '🔥',
|
| 109 |
+
'title': 'Conduction Thermique',
|
| 110 |
+
'metrics': ['T_initial', 'T_surface', 'k', 'theta', 'error_max'],
|
| 111 |
+
},
|
| 112 |
+
'traffic-flow': {
|
| 113 |
+
'icon': '🚗',
|
| 114 |
+
'title': 'Flux de Trafic',
|
| 115 |
+
'metrics': ['u_max', 'rho_max', 'max_density', 'avg_density'],
|
| 116 |
+
},
|
| 117 |
+
'phugoid': {
|
| 118 |
+
'icon': '✈️',
|
| 119 |
+
'title': 'Trajectoire de Vol',
|
| 120 |
+
'metrics': ['C', 'zt', 'z0', 'theta0', 'theta_final'],
|
| 121 |
+
},
|
| 122 |
+
'elasticity-dangvan': {
|
| 123 |
+
'icon': '⚙️',
|
| 124 |
+
'title': 'Élasticité',
|
| 125 |
+
'metrics': ['trace', 'hydrostatic_pressure', 'tresca', 'von_mises'],
|
| 126 |
+
},
|
| 127 |
+
'wave-equation': {
|
| 128 |
+
'icon': '🌊',
|
| 129 |
+
'title': 'Équation d\'Onde',
|
| 130 |
+
'metrics': ['wave_speed', 'dx', 'dt', 'CFL', 'max_displacement'],
|
| 131 |
+
},
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
def get_display_config(slug):
|
| 136 |
+
"""Récupère la configuration d'affichage pour une méthode."""
|
| 137 |
+
return DISPLAY_CONFIG.get(slug, {
|
| 138 |
+
'icon': '📊',
|
| 139 |
+
'title': slug.replace('-', ' ').title(),
|
| 140 |
+
'metrics': [],
|
| 141 |
+
})
|
| 142 |
+
|
| 143 |
+
|
| 144 |
+
# Import pour éviter les dépendances circulaires
|
| 145 |
+
import json
|
| 146 |
+
|
| 147 |
+
|
| 148 |
+
def run_simulation_safe(run_id):
|
| 149 |
+
"""Exécute une simulation de manière sécurisée avec gestion des erreurs."""
|
| 150 |
+
try:
|
| 151 |
+
run = SimulationRun.objects.get(pk=run_id)
|
| 152 |
+
method_func = SIMULATION_METHODS.get(run.method.slug)
|
| 153 |
+
|
| 154 |
+
if not method_func:
|
| 155 |
+
raise ValueError(f"Méthode inconnue: {run.method.slug}")
|
| 156 |
+
|
| 157 |
+
gen = method_func(run.parameters)
|
| 158 |
+
result = None
|
| 159 |
+
|
| 160 |
+
try:
|
| 161 |
+
while True:
|
| 162 |
+
progress = next(gen)
|
| 163 |
+
run.progress = progress
|
| 164 |
+
run.save(update_fields=['progress'])
|
| 165 |
+
except StopIteration as e:
|
| 166 |
+
result = e.value
|
| 167 |
+
|
| 168 |
+
if result is None:
|
| 169 |
+
result = {}
|
| 170 |
+
|
| 171 |
+
# Nettoyer les valeurs NaN/Inf
|
| 172 |
+
from simulations.tasks import clean_for_json
|
| 173 |
+
result = clean_for_json(result)
|
| 174 |
+
|
| 175 |
+
run.set_success(result)
|
| 176 |
+
return {'success': True, 'run_id': run_id}
|
| 177 |
+
|
| 178 |
+
except Exception as e:
|
| 179 |
+
if 'run' in locals():
|
| 180 |
+
run.set_failure(str(e))
|
| 181 |
+
return {'success': False, 'error': str(e), 'run_id': run_id}
|
simulations/serializers.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rest_framework import serializers
|
| 2 |
+
from .models import SimulationMethod, SimulationRun
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
class SimulationMethodSerializer(serializers.ModelSerializer):
|
| 6 |
+
class Meta:
|
| 7 |
+
model = SimulationMethod
|
| 8 |
+
fields = [
|
| 9 |
+
'id', 'name', 'slug', 'description', 'theory',
|
| 10 |
+
'parameters_schema', 'default_parameters',
|
| 11 |
+
'is_active', 'created_at', 'updated_at'
|
| 12 |
+
]
|
| 13 |
+
read_only_fields = ['id', 'created_at', 'updated_at']
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class SimulationRunSerializer(serializers.ModelSerializer):
|
| 17 |
+
method_name = serializers.CharField(source='method.name', read_only=True)
|
| 18 |
+
method_slug = serializers.CharField(source='method.slug', read_only=True)
|
| 19 |
+
|
| 20 |
+
class Meta:
|
| 21 |
+
model = SimulationRun
|
| 22 |
+
fields = [
|
| 23 |
+
'id', 'method', 'method_name', 'method_slug',
|
| 24 |
+
'name', 'parameters', 'status', 'progress',
|
| 25 |
+
'logs', 'result_data', 'error_message',
|
| 26 |
+
'input_file', 'output_file', 'created_at',
|
| 27 |
+
'started_at', 'completed_at'
|
| 28 |
+
]
|
| 29 |
+
read_only_fields = [
|
| 30 |
+
'id', 'status', 'progress', 'logs',
|
| 31 |
+
'result_data', 'error_message', 'output_file',
|
| 32 |
+
'created_at', 'started_at', 'completed_at'
|
| 33 |
+
]
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
class SimulationRunCreateSerializer(serializers.ModelSerializer):
|
| 37 |
+
class Meta:
|
| 38 |
+
model = SimulationRun
|
| 39 |
+
fields = ['method', 'name', 'parameters', 'input_file']
|
| 40 |
+
|
| 41 |
+
def validate_parameters(self, value):
|
| 42 |
+
method = self.initial_data.get('method')
|
| 43 |
+
if method:
|
| 44 |
+
try:
|
| 45 |
+
sim_method = SimulationMethod.objects.get(pk=method)
|
| 46 |
+
schema = sim_method.parameters_schema
|
| 47 |
+
except SimulationMethod.DoesNotExist:
|
| 48 |
+
pass
|
| 49 |
+
return value
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
class SimulationRunDetailSerializer(serializers.ModelSerializer):
|
| 53 |
+
method = SimulationMethodSerializer(read_only=True)
|
| 54 |
+
|
| 55 |
+
class Meta:
|
| 56 |
+
model = SimulationRun
|
| 57 |
+
fields = '__all__'
|
simulations/tasks.py
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import time
|
| 2 |
+
import random
|
| 3 |
+
import math
|
| 4 |
+
from celery import shared_task
|
| 5 |
+
import json
|
| 6 |
+
import numpy as np
|
| 7 |
+
import matplotlib.pyplot as plt
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def clean_for_json(obj):
|
| 11 |
+
"""Convertit les objets numpy en types Python valides pour JSON."""
|
| 12 |
+
if isinstance(obj, np.ndarray):
|
| 13 |
+
return [clean_for_json(x) for x in obj.tolist()]
|
| 14 |
+
elif isinstance(obj, (np.integer,)):
|
| 15 |
+
return int(obj)
|
| 16 |
+
elif isinstance(obj, (np.floating,)):
|
| 17 |
+
val = float(obj)
|
| 18 |
+
if math.isnan(val) or math.isinf(val):
|
| 19 |
+
return 0.0
|
| 20 |
+
return val
|
| 21 |
+
elif isinstance(obj, dict):
|
| 22 |
+
return {k: clean_for_json(v) for k, v in obj.items()}
|
| 23 |
+
elif isinstance(obj, list):
|
| 24 |
+
return [clean_for_json(x) for x in obj]
|
| 25 |
+
elif isinstance(obj, (int, float, str, bool, type(None))):
|
| 26 |
+
return obj
|
| 27 |
+
else:
|
| 28 |
+
return obj
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def run_monte_carlo_simulation(params):
|
| 32 |
+
"""Simulation Monte Carlo pour estimation de Pi."""
|
| 33 |
+
n_points = params.get('n_points', 10000)
|
| 34 |
+
seed = params.get('seed', 42)
|
| 35 |
+
|
| 36 |
+
random.seed(seed)
|
| 37 |
+
inside_circle = 0
|
| 38 |
+
|
| 39 |
+
for i in range(n_points):
|
| 40 |
+
x = random.random()
|
| 41 |
+
y = random.random()
|
| 42 |
+
if x*x + y*y <= 1:
|
| 43 |
+
inside_circle += 1
|
| 44 |
+
if (i + 1) % (n_points // 10) == 0:
|
| 45 |
+
progress = int((i + 1) / n_points * 100)
|
| 46 |
+
yield progress
|
| 47 |
+
|
| 48 |
+
pi_estimate = 4 * inside_circle / n_points
|
| 49 |
+
return {
|
| 50 |
+
'pi_estimate': clean_for_json(pi_estimate),
|
| 51 |
+
'exact_pi': math.pi,
|
| 52 |
+
'error': clean_for_json(abs(pi_estimate - math.pi)),
|
| 53 |
+
'n_points': n_points,
|
| 54 |
+
'inside_circle': inside_circle
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def run_diffusion_1d_simulation(params):
|
| 59 |
+
"""Simulation de diffusion 1D explicite."""
|
| 60 |
+
L = params.get('length', 1.0)
|
| 61 |
+
T = params.get('time', 0.1)
|
| 62 |
+
nx = params.get('nx', 50)
|
| 63 |
+
nt = params.get('nt', 100)
|
| 64 |
+
D = params.get('diffusion_coef', 1.0)
|
| 65 |
+
|
| 66 |
+
dx = L / (nx - 1)
|
| 67 |
+
dt = T / nt
|
| 68 |
+
|
| 69 |
+
u = [0.0] * nx
|
| 70 |
+
u[0] = 1.0
|
| 71 |
+
u[-1] = 0.0
|
| 72 |
+
|
| 73 |
+
for j in range(nt):
|
| 74 |
+
u_old = u.copy()
|
| 75 |
+
for i in range(1, nx - 1):
|
| 76 |
+
u[i] = u_old[i] + D * dt / (dx * dx) * (
|
| 77 |
+
u_old[i+1] - 2*u_old[i] + u_old[i-1]
|
| 78 |
+
)
|
| 79 |
+
if (j + 1) % (nt // 10) == 0:
|
| 80 |
+
progress = int((j + 1) / nt * 100)
|
| 81 |
+
yield progress
|
| 82 |
+
|
| 83 |
+
return {
|
| 84 |
+
'final_state': clean_for_json(u),
|
| 85 |
+
'dx': clean_for_json(dx),
|
| 86 |
+
'dt': clean_for_json(dt),
|
| 87 |
+
'nx': nx,
|
| 88 |
+
'nt': nt,
|
| 89 |
+
'max_value': clean_for_json(max(u)),
|
| 90 |
+
'min_value': clean_for_json(min(u))
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def run_linear_solve_simulation(params):
|
| 95 |
+
"""Résolution de système linéaire Ax = b."""
|
| 96 |
+
size = params.get('size', 100)
|
| 97 |
+
|
| 98 |
+
np.random.seed(params.get('seed', 42))
|
| 99 |
+
|
| 100 |
+
A = np.random.rand(size, size) + size * np.eye(size)
|
| 101 |
+
b = np.random.rand(size)
|
| 102 |
+
|
| 103 |
+
for i in range(5):
|
| 104 |
+
time.sleep(0.5)
|
| 105 |
+
yield (i + 1) * 20
|
| 106 |
+
|
| 107 |
+
x = np.linalg.solve(A, b)
|
| 108 |
+
residual = np.linalg.norm(A.dot(x) - b)
|
| 109 |
+
|
| 110 |
+
return {
|
| 111 |
+
'solution_norm': clean_for_json(np.linalg.norm(x)),
|
| 112 |
+
'residual': clean_for_json(residual),
|
| 113 |
+
'condition_number': clean_for_json(np.linalg.cond(A)),
|
| 114 |
+
'size': size
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
|
| 118 |
+
def run_heat_conduction_simulation(params):
|
| 119 |
+
"""Simulation de conduction thermique Newton (finite_difference)."""
|
| 120 |
+
T_0 = params.get('T_initial', 100.0)
|
| 121 |
+
T_s = params.get('T_surface', 20.0)
|
| 122 |
+
k = params.get('conductivity', 0.1)
|
| 123 |
+
dt = params.get('dt', 0.001)
|
| 124 |
+
N = params.get('N', 1000)
|
| 125 |
+
theta = params.get('theta', 0.5)
|
| 126 |
+
|
| 127 |
+
T = np.zeros(N)
|
| 128 |
+
T[0] = T_0
|
| 129 |
+
|
| 130 |
+
for i in range(1, N):
|
| 131 |
+
T[i] = (1 - (1 - theta) * dt * k) * T[i-1] / (1 + theta * dt * k) - k * dt * T_s / (1 + theta * dt * k)
|
| 132 |
+
if (i + 1) % (N // 10) == 0:
|
| 133 |
+
progress = int((i + 1) / N * 100)
|
| 134 |
+
yield progress
|
| 135 |
+
|
| 136 |
+
T_analytical = (T_0 - T_s) * np.exp(-k * np.arange(N) * dt) + T_s
|
| 137 |
+
error = np.max(np.abs(T - T_analytical))
|
| 138 |
+
|
| 139 |
+
return {
|
| 140 |
+
'final_T': clean_for_json(T.tolist()),
|
| 141 |
+
'T_analytical': clean_for_json(T_analytical.tolist()),
|
| 142 |
+
'error_max': clean_for_json(error),
|
| 143 |
+
'T_initial': T_0,
|
| 144 |
+
'T_surface': T_s,
|
| 145 |
+
'k': k,
|
| 146 |
+
'dt': dt,
|
| 147 |
+
'theta': theta,
|
| 148 |
+
'N': N,
|
| 149 |
+
'time_final': clean_for_json(N * dt)
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def run_traffic_flow_simulation(params):
|
| 154 |
+
"""Modèle de trafic 1D (numerical-mooc)."""
|
| 155 |
+
nx = params.get('nx', 100)
|
| 156 |
+
nt = params.get('nt', 200)
|
| 157 |
+
L = params.get('length', 10.0)
|
| 158 |
+
T = params.get('time', 2.0)
|
| 159 |
+
u_max = params.get('u_max', 1.0)
|
| 160 |
+
rho_max = params.get('rho_max', 1.0)
|
| 161 |
+
|
| 162 |
+
dx = L / nx
|
| 163 |
+
dt = T / nt
|
| 164 |
+
|
| 165 |
+
x = np.linspace(0, L, nx)
|
| 166 |
+
t = np.linspace(0, T, nt)
|
| 167 |
+
|
| 168 |
+
rho = np.zeros((nt, nx))
|
| 169 |
+
rho[0] = rho_red_light(x, rho_max)
|
| 170 |
+
|
| 171 |
+
for n in range(nt - 1):
|
| 172 |
+
for i in range(nx):
|
| 173 |
+
F_ip = flux(rho[n, (i+1) % nx], u_max, rho_max)
|
| 174 |
+
F_i = flux(rho[n, i], u_max, rho_max)
|
| 175 |
+
rho[n+1, i] = rho[n, i] + dt / dx * (F_i - F_ip)
|
| 176 |
+
|
| 177 |
+
if (n + 1) % (nt // 10) == 0:
|
| 178 |
+
progress = int((n + 1) / nt * 100)
|
| 179 |
+
yield progress
|
| 180 |
+
|
| 181 |
+
return {
|
| 182 |
+
'density_matrix': clean_for_json(rho.tolist()),
|
| 183 |
+
'x_grid': clean_for_json(x.tolist()),
|
| 184 |
+
't_grid': clean_for_json(t.tolist()),
|
| 185 |
+
'u_max': u_max,
|
| 186 |
+
'rho_max': rho_max,
|
| 187 |
+
'max_density': clean_for_json(float(np.max(rho))),
|
| 188 |
+
'avg_density': clean_for_json(float(np.mean(rho)))
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def rho_red_light(x, rho_max):
|
| 193 |
+
"""Condition initiale 'red light' avec choc."""
|
| 194 |
+
rho = rho_max * np.ones_like(x)
|
| 195 |
+
mask = np.where(x < 3.0)
|
| 196 |
+
rho[mask] = 0.5 * rho_max
|
| 197 |
+
return rho
|
| 198 |
+
|
| 199 |
+
|
| 200 |
+
def flux(rho, u_max, rho_max):
|
| 201 |
+
"""Flux de trafic F = V * rho."""
|
| 202 |
+
return rho * u_max * (1.0 - rho / rho_max)
|
| 203 |
+
|
| 204 |
+
|
| 205 |
+
def run_phugoid_simulation(params):
|
| 206 |
+
"""Trajectoire de vol phugoid (numerical-mooc)."""
|
| 207 |
+
zt = params.get('zt', 1.0)
|
| 208 |
+
z0 = params.get('z0', 2.0)
|
| 209 |
+
theta0 = params.get('theta0', 5.0)
|
| 210 |
+
N = params.get('N', 1000)
|
| 211 |
+
|
| 212 |
+
theta0_rad = np.radians(theta0)
|
| 213 |
+
|
| 214 |
+
if z0 <= 0:
|
| 215 |
+
z0 = 0.1
|
| 216 |
+
|
| 217 |
+
try:
|
| 218 |
+
C = (np.cos(theta0_rad) - 1/3 * z0 / zt) * (z0 / zt)**0.5
|
| 219 |
+
except (ZeroDivisionError, RuntimeWarning):
|
| 220 |
+
C = 0.0
|
| 221 |
+
|
| 222 |
+
x = np.zeros(N)
|
| 223 |
+
z = np.zeros(N)
|
| 224 |
+
theta = theta0_rad
|
| 225 |
+
|
| 226 |
+
for i in range(1, N):
|
| 227 |
+
try:
|
| 228 |
+
normal = np.array([+ np.cos(theta + np.pi/2.0), - np.sin(theta + np.pi/2.0)])
|
| 229 |
+
R = radius_of_curvature(z[i-1], zt, C)
|
| 230 |
+
if abs(R) < 0.001:
|
| 231 |
+
R = 0.001
|
| 232 |
+
center = np.array([x[i-1], z[i-1]]) + R * normal
|
| 233 |
+
ds = 1.0
|
| 234 |
+
dtheta = ds / R
|
| 235 |
+
x[i], z[i] = rotate((x[i-1], z[i-1]), center=center, angle=dtheta, mode='radians')
|
| 236 |
+
theta += dtheta
|
| 237 |
+
except (RuntimeWarning, FloatingPointError):
|
| 238 |
+
break
|
| 239 |
+
|
| 240 |
+
if i % (N // 10) == 0:
|
| 241 |
+
yield int(i / N * 100)
|
| 242 |
+
|
| 243 |
+
return {
|
| 244 |
+
'x': clean_for_json(x.tolist()),
|
| 245 |
+
'z': clean_for_json(z.tolist()),
|
| 246 |
+
'theta_final': clean_for_json(float(np.degrees(theta))),
|
| 247 |
+
'C': clean_for_json(float(C)),
|
| 248 |
+
'zt': zt,
|
| 249 |
+
'z0': z0,
|
| 250 |
+
'theta0': theta0
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
|
| 254 |
+
def radius_of_curvature(z, zt, C):
|
| 255 |
+
"""Rayon de courbure de la trajectoire."""
|
| 256 |
+
if z <= 0 or abs(1/3 - C / 2 * (zt / z)**1.5) < 1e-10:
|
| 257 |
+
return zt * 1000
|
| 258 |
+
return zt / (1 / 3 - C / 2 * (zt / z)**1.5)
|
| 259 |
+
|
| 260 |
+
|
| 261 |
+
def rotate(coords, center=(0.0, 0.0), angle=0.0, mode='degrees'):
|
| 262 |
+
"""Rotation d'un point autour d'un centre."""
|
| 263 |
+
x, z = coords
|
| 264 |
+
xc, zc = center
|
| 265 |
+
if mode == 'degrees':
|
| 266 |
+
angle = np.radians(angle)
|
| 267 |
+
x_new = xc + (x - xc) * np.cos(angle) + (z - zc) * np.sin(angle)
|
| 268 |
+
z_new = zc - (x - xc) * np.sin(angle) + (z - zc) * np.cos(angle)
|
| 269 |
+
return x_new, z_new
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def run_elasticity_dangvan_simulation(params):
|
| 273 |
+
"""Tenseur deviatoire et critère de Tresca (DangVan)."""
|
| 274 |
+
tensor_input = params.get('tensor', [100, 0, 0, 0, 50, 0, 0, 0, 30])
|
| 275 |
+
sigma = np.array(tensor_input).reshape(3, 3)
|
| 276 |
+
trace = np.trace(sigma)
|
| 277 |
+
hydrostatic = (trace / 3) * np.eye(3)
|
| 278 |
+
deviator = sigma - hydrostatic
|
| 279 |
+
eigenvals = np.linalg.eigvals(sigma)
|
| 280 |
+
|
| 281 |
+
yield 30
|
| 282 |
+
|
| 283 |
+
tresca = np.max(eigenvals) - np.min(eigenvals)
|
| 284 |
+
von_mises = np.sqrt(3/2 * np.sum(deviator**2))
|
| 285 |
+
|
| 286 |
+
yield 60
|
| 287 |
+
|
| 288 |
+
yield 100
|
| 289 |
+
|
| 290 |
+
return {
|
| 291 |
+
'tensor_matrix': clean_for_json(sigma.tolist()),
|
| 292 |
+
'deviator_tensor': clean_for_json(deviator.tolist()),
|
| 293 |
+
'principal_stresses': clean_for_json(eigenvals.tolist()),
|
| 294 |
+
'trace': clean_for_json(float(trace)),
|
| 295 |
+
'hydrostatic_pressure': clean_for_json(float(trace / 3)),
|
| 296 |
+
'tresca': clean_for_json(float(tresca)),
|
| 297 |
+
'von_mises': clean_for_json(float(von_mises))
|
| 298 |
+
}
|
| 299 |
+
|
| 300 |
+
|
| 301 |
+
def run_wave_equation_simulation(params):
|
| 302 |
+
"""Résolution de l'équation d'onde 1D."""
|
| 303 |
+
L = params.get('length', 1.0)
|
| 304 |
+
T = params.get('time', 1.0)
|
| 305 |
+
nx = params.get('nx', 100)
|
| 306 |
+
nt = params.get('nt', 500)
|
| 307 |
+
c = params.get('wave_speed', 1.0)
|
| 308 |
+
|
| 309 |
+
dx = L / nx
|
| 310 |
+
dt = T / nt
|
| 311 |
+
r = c * dt / dx
|
| 312 |
+
|
| 313 |
+
if r > 1.0:
|
| 314 |
+
r = 0.9
|
| 315 |
+
|
| 316 |
+
x = np.linspace(0, L, nx)
|
| 317 |
+
t = np.linspace(0, T, nt)
|
| 318 |
+
|
| 319 |
+
u = np.zeros((nt, nx))
|
| 320 |
+
u[0] = np.sin(np.pi * x / L)
|
| 321 |
+
u[1] = 0.5 * u[0].copy()
|
| 322 |
+
|
| 323 |
+
for n in range(1, nt - 1):
|
| 324 |
+
for i in range(1, nx - 1):
|
| 325 |
+
u[n+1, i] = 2*(1 - r**2) * u[n, i] - u[n-1, i] + r**2 * (u[n, i+1] + u[n, i-1])
|
| 326 |
+
u[n+1, 0] = 0
|
| 327 |
+
u[n+1, -1] = 0
|
| 328 |
+
|
| 329 |
+
if (n + 1) % (nt // 10) == 0:
|
| 330 |
+
progress = int((n + 1) / nt * 100)
|
| 331 |
+
yield progress
|
| 332 |
+
|
| 333 |
+
max_displacement = clean_for_json(float(np.max(np.abs(u))))
|
| 334 |
+
|
| 335 |
+
return {
|
| 336 |
+
'displacement_matrix': clean_for_json(u.tolist()),
|
| 337 |
+
'x_grid': clean_for_json(x.tolist()),
|
| 338 |
+
't_grid': clean_for_json(t.tolist()),
|
| 339 |
+
'wave_speed': c,
|
| 340 |
+
'dx': clean_for_json(dx),
|
| 341 |
+
'dt': clean_for_json(dt),
|
| 342 |
+
'CFL': clean_for_json(r),
|
| 343 |
+
'max_displacement': max_displacement
|
| 344 |
+
}
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
SIMULATION_METHODS = {
|
| 348 |
+
'monte-carlo-pi': run_monte_carlo_simulation,
|
| 349 |
+
'diffusion-1d': run_diffusion_1d_simulation,
|
| 350 |
+
'linear-solve': run_linear_solve_simulation,
|
| 351 |
+
'heat-conduction': run_heat_conduction_simulation,
|
| 352 |
+
'traffic-flow': run_traffic_flow_simulation,
|
| 353 |
+
'phugoid': run_phugoid_simulation,
|
| 354 |
+
'elasticity-dangvan': run_elasticity_dangvan_simulation,
|
| 355 |
+
'wave-equation': run_wave_equation_simulation,
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
|
| 359 |
+
@shared_task(bind=True)
|
| 360 |
+
def run_simulation(self, run_id):
|
| 361 |
+
"""Tâche Celery pour exécuter une simulation."""
|
| 362 |
+
from simulations.models import SimulationRun
|
| 363 |
+
|
| 364 |
+
try:
|
| 365 |
+
run = SimulationRun.objects.get(pk=run_id)
|
| 366 |
+
run.set_running()
|
| 367 |
+
run.add_log(f"Début de la simulation {run_id}")
|
| 368 |
+
run.add_log(f"Paramètres: {json.dumps(run.parameters)}")
|
| 369 |
+
|
| 370 |
+
method_func = SIMULATION_METHODS.get(run.method.slug)
|
| 371 |
+
|
| 372 |
+
if not method_func:
|
| 373 |
+
raise ValueError(f"Méthode inconnue: {run.method.slug}")
|
| 374 |
+
|
| 375 |
+
gen = method_func(run.parameters)
|
| 376 |
+
result = None
|
| 377 |
+
|
| 378 |
+
try:
|
| 379 |
+
while True:
|
| 380 |
+
progress = next(gen)
|
| 381 |
+
run.progress = progress
|
| 382 |
+
run.save(update_fields=['progress'])
|
| 383 |
+
except StopIteration as e:
|
| 384 |
+
result = e.value
|
| 385 |
+
|
| 386 |
+
if result is None:
|
| 387 |
+
result = {}
|
| 388 |
+
|
| 389 |
+
result = clean_for_json(result)
|
| 390 |
+
|
| 391 |
+
run.set_success(result)
|
| 392 |
+
run.add_log(f"Simulation terminée avec succès")
|
| 393 |
+
run.add_log(f"Résultat: {json.dumps(result)}")
|
| 394 |
+
|
| 395 |
+
except Exception as e:
|
| 396 |
+
if 'run' in locals():
|
| 397 |
+
run.set_failure(str(e))
|
| 398 |
+
run.add_log(f"Erreur: {str(e)}")
|
| 399 |
+
raise
|
| 400 |
+
|
| 401 |
+
|
| 402 |
+
@shared_task
|
| 403 |
+
def cleanup_old_runs(days=7):
|
| 404 |
+
"""Nettoie les simulations anciennes."""
|
| 405 |
+
from simulations.models import SimulationRun
|
| 406 |
+
from django.utils import timezone
|
| 407 |
+
from datetime import timedelta
|
| 408 |
+
|
| 409 |
+
cutoff = timezone.now() - timedelta(days=days)
|
| 410 |
+
old_runs = SimulationRun.objects.filter(
|
| 411 |
+
created_at__lt=cutoff,
|
| 412 |
+
status__in=['SUCCESS', 'FAILURE', 'CANCELLED']
|
| 413 |
+
)
|
| 414 |
+
|
| 415 |
+
count = old_runs.count()
|
| 416 |
+
old_runs.delete()
|
| 417 |
+
|
| 418 |
+
return f"Supprimé {count} simulations anciennes"
|
simulations/templates/simulations/base.html
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="fr">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{% block title %}Simulations Numériques{% endblock %}</title>
|
| 7 |
+
<style>
|
| 8 |
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
| 9 |
+
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; background: #f5f5f5; }
|
| 10 |
+
header { background: #2c3e50; color: white; padding: 1rem 2rem; }
|
| 11 |
+
nav a { color: white; text-decoration: none; margin-right: 1.5rem; }
|
| 12 |
+
nav a:hover { text-decoration: underline; }
|
| 13 |
+
main { max-width: 1200px; margin: 0 auto; padding: 2rem; }
|
| 14 |
+
.card { background: white; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); padding: 1.5rem; margin-bottom: 1.5rem; }
|
| 15 |
+
.btn { display: inline-block; padding: 0.5rem 1rem; background: #3498db; color: white; text-decoration: none; border-radius: 4px; border: none; cursor: pointer; }
|
| 16 |
+
.btn:hover { background: #2980b9; }
|
| 17 |
+
.btn-secondary { background: #95a5a6; }
|
| 18 |
+
.btn-danger { background: #e74c3c; }
|
| 19 |
+
.btn-success { background: #27ae60; }
|
| 20 |
+
.status-pending { color: #f39c12; }
|
| 21 |
+
.status-running { color: #3498db; }
|
| 22 |
+
.status-success { color: #27ae60; }
|
| 23 |
+
.status-failure { color: #e74c3c; }
|
| 24 |
+
.status-cancelled { color: #95a5a6; }
|
| 25 |
+
table { width: 100%; border-collapse: collapse; }
|
| 26 |
+
th, td { padding: 0.75rem; text-align: left; border-bottom: 1px solid #eee; }
|
| 27 |
+
.progress-bar { background: #ecf0f1; border-radius: 4px; height: 20px; overflow: hidden; }
|
| 28 |
+
.progress-fill { background: #27ae60; height: 100%; transition: width 0.3s; }
|
| 29 |
+
.logs { background: #2c3e50; color: #ecf0f1; padding: 1rem; border-radius: 4px; font-family: monospace; white-space: pre-wrap; max-height: 300px; overflow-y: auto; }
|
| 30 |
+
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1.5rem; }
|
| 31 |
+
.method-card { transition: transform 0.2s; }
|
| 32 |
+
.method-card:hover { transform: translateY(-2px); }
|
| 33 |
+
.flash-messages { margin-bottom: 1rem; }
|
| 34 |
+
.flash { padding: 1rem; border-radius: 4px; margin-bottom: 0.5rem; }
|
| 35 |
+
.flash-success { background: #d4edda; color: #155724; }
|
| 36 |
+
.flash-error { background: #f8d7da; color: #721c24; }
|
| 37 |
+
</style>
|
| 38 |
+
</head>
|
| 39 |
+
<body>
|
| 40 |
+
<header>
|
| 41 |
+
<nav>
|
| 42 |
+
<a href="{% url 'home' %}">Accueil</a>
|
| 43 |
+
<a href="{% url 'method_list' %}">Méthodes</a>
|
| 44 |
+
<a href="{% url 'run_create' %}">Nouvelle Simulation</a>
|
| 45 |
+
<a href="{% url 'run_list' %}">Historique</a>
|
| 46 |
+
</nav>
|
| 47 |
+
</header>
|
| 48 |
+
<main>
|
| 49 |
+
{% if messages %}
|
| 50 |
+
<div class="flash-messages">
|
| 51 |
+
{% for message in messages %}
|
| 52 |
+
<div class="flash flash-{{ message.tags }}">{{ message }}</div>
|
| 53 |
+
{% endfor %}
|
| 54 |
+
</div>
|
| 55 |
+
{% endif %}
|
| 56 |
+
{% block content %}{% endblock %}
|
| 57 |
+
</main>
|
| 58 |
+
<script>
|
| 59 |
+
function pollStatus(runId) {
|
| 60 |
+
fetch(`/run/${runId}/status/`)
|
| 61 |
+
.then(r => r.json())
|
| 62 |
+
.then(data => {
|
| 63 |
+
document.getElementById('status').textContent = data.status;
|
| 64 |
+
document.getElementById('progress').textContent = data.progress + '%';
|
| 65 |
+
document.getElementById('progress-fill').style.width = data.progress + '%';
|
| 66 |
+
if (data.status === 'RUNNING') {
|
| 67 |
+
setTimeout(() => pollStatus(runId), 1000);
|
| 68 |
+
} else {
|
| 69 |
+
location.reload();
|
| 70 |
+
}
|
| 71 |
+
});
|
| 72 |
+
}
|
| 73 |
+
{% if run and run.status == 'RUNNING' %}
|
| 74 |
+
pollStatus({{ run.id }});
|
| 75 |
+
{% endif %}
|
| 76 |
+
</script>
|
| 77 |
+
</body>
|
| 78 |
+
</html>
|
simulations/templates/simulations/home.html
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'simulations/base.html' %}
|
| 2 |
+
{% block title %}Accueil - Simulations Numériques{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<h1>Simulations Numériques</h1>
|
| 5 |
+
<p>Plateforme d'exécution de méthodes de simulation numérique avec traitement asynchrone.</p>
|
| 6 |
+
|
| 7 |
+
<div class="grid">
|
| 8 |
+
{% for method in methods %}
|
| 9 |
+
<div class="card method-card">
|
| 10 |
+
<h3>{{ method.name }}</h3>
|
| 11 |
+
<p>{{ method.description|truncatewords:20 }}</p>
|
| 12 |
+
<a href="{% url 'method_detail' method.slug %}" class="btn">Voir la méthode</a>
|
| 13 |
+
<a href="{% url 'run_create_method' method.slug %}" class="btn btn-success">Lancer</a>
|
| 14 |
+
</div>
|
| 15 |
+
{% empty %}
|
| 16 |
+
<p>Aucune méthode disponible.</p>
|
| 17 |
+
{% endfor %}
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
{% if recent_runs %}
|
| 21 |
+
<h2>Simulations récentes</h2>
|
| 22 |
+
<table>
|
| 23 |
+
<thead>
|
| 24 |
+
<tr>
|
| 25 |
+
<th>ID</th>
|
| 26 |
+
<th>Méthode</th>
|
| 27 |
+
<th>Statut</th>
|
| 28 |
+
<th>Progrès</th>
|
| 29 |
+
<th>Créé</th>
|
| 30 |
+
</tr>
|
| 31 |
+
</thead>
|
| 32 |
+
<tbody>
|
| 33 |
+
{% for run in recent_runs %}
|
| 34 |
+
<tr>
|
| 35 |
+
<td><a href="{% url 'run_detail' run.id %}">#{{ run.id }}</a></td>
|
| 36 |
+
<td>{{ run.method.name }}</td>
|
| 37 |
+
<td class="status-{{ run.status|lower }}">{{ run.get_status_display }}</td>
|
| 38 |
+
<td>{{ run.progress }}%</td>
|
| 39 |
+
<td>{{ run.created_at|date:"d/m/Y H:i" }}</td>
|
| 40 |
+
</tr>
|
| 41 |
+
{% endfor %}
|
| 42 |
+
</tbody>
|
| 43 |
+
</table>
|
| 44 |
+
{% endif %}
|
| 45 |
+
|
| 46 |
+
<div class="card">
|
| 47 |
+
<h2>Démarrage rapide</h2>
|
| 48 |
+
<ol>
|
| 49 |
+
<li>Choisissez une méthode de simulation</li>
|
| 50 |
+
<li>Configurez les paramètres</li>
|
| 51 |
+
<li>Lancez la simulation</li>
|
| 52 |
+
<li>Consultez les résultats</li>
|
| 53 |
+
</ol>
|
| 54 |
+
<a href="{% url 'run_create' %}" class="btn btn-success">Créer une simulation</a>
|
| 55 |
+
</div>
|
| 56 |
+
{% endblock %}
|
simulations/templates/simulations/method_detail.html
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'simulations/base.html' %}
|
| 2 |
+
{% load simulation_extras %}
|
| 3 |
+
{% block title %}{{ method.name }}{% endblock %}
|
| 4 |
+
{% block content %}
|
| 5 |
+
<h1>{{ method.name }}</h1>
|
| 6 |
+
<div class="card">
|
| 7 |
+
<h2>Description</h2>
|
| 8 |
+
<p>{{ method.description }}</p>
|
| 9 |
+
</div>
|
| 10 |
+
|
| 11 |
+
{% if method.theory %}
|
| 12 |
+
<div class="card">
|
| 13 |
+
<h2>Théorie</h2>
|
| 14 |
+
<div>{{ method.theory|linebreaks }}</div>
|
| 15 |
+
</div>
|
| 16 |
+
{% endif %}
|
| 17 |
+
|
| 18 |
+
<div class="card">
|
| 19 |
+
<h2>Paramètres par défaut</h2>
|
| 20 |
+
<pre>{{ method.default_parameters|json_format }}</pre>
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<a href="{% url 'run_create_method' method.slug %}" class="btn btn-success">Lancer une simulation</a>
|
| 24 |
+
{% endblock %}
|
simulations/templates/simulations/method_list.html
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'simulations/base.html' %}
|
| 2 |
+
{% block title %}Méthodes de Simulation{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<h1>Méthodes de Simulation</h1>
|
| 5 |
+
<div class="grid">
|
| 6 |
+
{% for method in methods %}
|
| 7 |
+
<div class="card method-card">
|
| 8 |
+
<h3>{{ method.name }}</h3>
|
| 9 |
+
<p>{{ method.description }}</p>
|
| 10 |
+
<a href="{% url 'method_detail' method.slug %}" class="btn">En savoir plus</a>
|
| 11 |
+
<a href="{% url 'run_create_method' method.slug %}" class="btn btn-success">Lancer</a>
|
| 12 |
+
</div>
|
| 13 |
+
{% empty %}
|
| 14 |
+
<p>Aucune méthode disponible.</p>
|
| 15 |
+
{% endfor %}
|
| 16 |
+
</div>
|
| 17 |
+
{% endblock %}
|
simulations/templates/simulations/run_create.html
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'simulations/base.html' %}
|
| 2 |
+
{% load simulation_extras %}
|
| 3 |
+
{% block title %}Nouvelle Simulation{% endblock %}
|
| 4 |
+
{% block content %}
|
| 5 |
+
<h1>Nouvelle Simulation</h1>
|
| 6 |
+
<form method="post" enctype="multipart/form-data" class="card">
|
| 7 |
+
{% csrf_token %}
|
| 8 |
+
<div>
|
| 9 |
+
<label for="method">Méthode:</label>
|
| 10 |
+
<select name="method" id="method" required onchange="updateParams()">
|
| 11 |
+
<option value="">Sélectionnez une méthode</option>
|
| 12 |
+
{% for method in methods %}
|
| 13 |
+
<option value="{{ method.id }}" {% if selected_method and selected_method.id == method.id %}selected{% endif %}>
|
| 14 |
+
{{ method.name }}
|
| 15 |
+
</option>
|
| 16 |
+
{% endfor %}
|
| 17 |
+
</select>
|
| 18 |
+
</div>
|
| 19 |
+
<div>
|
| 20 |
+
<label for="name">Nom (optionnel):</label>
|
| 21 |
+
<input type="text" name="name" id="name" placeholder="Ma simulation">
|
| 22 |
+
</div>
|
| 23 |
+
<div>
|
| 24 |
+
<label for="parameters">Paramètres (JSON):</label>
|
| 25 |
+
<textarea name="parameters" id="parameters" rows="10" placeholder='{"n_points": 10000}'>{}</textarea>
|
| 26 |
+
</div>
|
| 27 |
+
<div>
|
| 28 |
+
<label for="input_file">Fichier d'entrée (optionnel):</label>
|
| 29 |
+
<input type="file" name="input_file" id="input_file">
|
| 30 |
+
</div>
|
| 31 |
+
<button type="submit" class="btn btn-success">Lancer la simulation</button>
|
| 32 |
+
</form>
|
| 33 |
+
|
| 34 |
+
<script>
|
| 35 |
+
const methods = {
|
| 36 |
+
{% for method in methods %}
|
| 37 |
+
{{ method.id }}: {
|
| 38 |
+
name: "{{ method.name }}",
|
| 39 |
+
default_params: {{ method.default_parameters|json_format }}
|
| 40 |
+
}{% if not forloop.last %},{% endif %}
|
| 41 |
+
{% endfor %}
|
| 42 |
+
};
|
| 43 |
+
|
| 44 |
+
function updateParams() {
|
| 45 |
+
const methodId = document.getElementById('method').value;
|
| 46 |
+
if (methodId && methods[methodId]) {
|
| 47 |
+
document.getElementById('parameters').value = JSON.stringify(
|
| 48 |
+
methods[methodId].default_params, null, 2
|
| 49 |
+
);
|
| 50 |
+
}
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
{% if selected_method %}
|
| 54 |
+
updateParams();
|
| 55 |
+
{% endif %}
|
| 56 |
+
</script>
|
| 57 |
+
{% endblock %}
|
simulations/templates/simulations/run_detail.html
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'simulations/base.html' %}
|
| 2 |
+
{% load simulation_extras %}
|
| 3 |
+
{% block title %}Simulation #{{ run.id }}{% endblock %}
|
| 4 |
+
{% block content %}
|
| 5 |
+
<h1>Simulation #{{ run.id }}</h1>
|
| 6 |
+
|
| 7 |
+
<div class="card">
|
| 8 |
+
<h2>Informations</h2>
|
| 9 |
+
<table>
|
| 10 |
+
<tr><th>Méthode:</th><td>{{ run.method.name }}</td></tr>
|
| 11 |
+
<tr><th>Slug:</th><td><code>{{ run.method.slug }}</code></td></tr>
|
| 12 |
+
<tr><th>Statut:</th><td class="status-{{ run.status|lower }}" id="status">{{ run.get_status_display }}</td></tr>
|
| 13 |
+
<tr><th>Progrès:</th><td><span id="progress">{{ run.progress }}</span>%</td></tr>
|
| 14 |
+
<tr><th>Créé le:</th><td>{{ run.created_at|date:"d/m/Y H:i:s" }}</td></tr>
|
| 15 |
+
<tr><th>Terminé le:</th><td>{{ run.completed_at|date:"d/m/Y H:i:s"|default:"-" }}</td></tr>
|
| 16 |
+
</table>
|
| 17 |
+
|
| 18 |
+
<div class="progress-bar" style="margin-top: 1rem;">
|
| 19 |
+
<div class="progress-fill" id="progress-fill" style="width: {{ run.progress }}%; transition: width 0.5s;"></div>
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
<div id="loading-indicator" style="display: none; margin-top: 1rem; text-align: center;">
|
| 23 |
+
<p>⏳ Simulation en cours... La page se rafraîchira automatiquement.</p>
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<div class="card">
|
| 28 |
+
<h2>Paramètres</h2>
|
| 29 |
+
{% if run.parameters %}
|
| 30 |
+
<table style="width: 100%;">
|
| 31 |
+
<thead><tr><th>Paramètre</th><th>Valeur</th></tr></thead>
|
| 32 |
+
<tbody>
|
| 33 |
+
{% for key, value in run.parameters.items %}
|
| 34 |
+
<tr><td>{{ key }}</td><td>{{ value }}</td></tr>
|
| 35 |
+
{% endfor %}
|
| 36 |
+
</tbody>
|
| 37 |
+
</table>
|
| 38 |
+
{% else %}
|
| 39 |
+
<p>Aucun paramètre personnalisé</p>
|
| 40 |
+
{% endif %}
|
| 41 |
+
</div>
|
| 42 |
+
|
| 43 |
+
{% if run.error_message %}
|
| 44 |
+
<div class="card" id="error-section" style="display: none;">
|
| 45 |
+
<h2>Erreur</h2>
|
| 46 |
+
<p style="color: #e74c3c;" id="error-message">{{ run.error_message }}</p>
|
| 47 |
+
</div>
|
| 48 |
+
{% endif %}
|
| 49 |
+
|
| 50 |
+
{% if run.result_data %}
|
| 51 |
+
<div class="card" id="results-section">
|
| 52 |
+
<h2>Résultats</h2>
|
| 53 |
+
|
| 54 |
+
{# Tableau générique pour tous les résultats #}
|
| 55 |
+
<table style="width: 100%;">
|
| 56 |
+
<thead><tr><th>Paramètre</th><th>Valeur</th></tr></thead>
|
| 57 |
+
<tbody>
|
| 58 |
+
{% for key, value in run.result_data.items %}
|
| 59 |
+
<tr>
|
| 60 |
+
<td>{{ key }}</td>
|
| 61 |
+
<td>
|
| 62 |
+
{% if value|is_list %}
|
| 63 |
+
<table style="width: 100%; font-size: 0.9em;">
|
| 64 |
+
{% for item in value %}
|
| 65 |
+
<tr><td>{{ item }}</td></tr>
|
| 66 |
+
{% endfor %}
|
| 67 |
+
</table>
|
| 68 |
+
{% elif value|is_dict %}
|
| 69 |
+
<pre>{{ value|json_format }}</pre>
|
| 70 |
+
{% elif key|contains:"matrix" or key|contains:"grid" or key|contains:"state" %}
|
| 71 |
+
<span style="color: #7f8c8d;">[Tableau {{ value|length }} éléments]</span>
|
| 72 |
+
{% else %}
|
| 73 |
+
{{ value|sigfig:2 }}
|
| 74 |
+
{% endif %}
|
| 75 |
+
</td>
|
| 76 |
+
</tr>
|
| 77 |
+
{% endfor %}
|
| 78 |
+
</tbody>
|
| 79 |
+
</table>
|
| 80 |
+
</div>
|
| 81 |
+
{% endif %}
|
| 82 |
+
|
| 83 |
+
{% if run.plot_file %}
|
| 84 |
+
<div class="card">
|
| 85 |
+
<h2>Graphique</h2>
|
| 86 |
+
<img src="{{ run.plot_file.url }}" alt="Graphique de la simulation" style="max-width: 100%; height: auto; border: 1px solid #ddd; border-radius: 4px;">
|
| 87 |
+
</div>
|
| 88 |
+
{% endif %}
|
| 89 |
+
|
| 90 |
+
{% if run.pdf_file or run.csv_file or run.plot_file %}
|
| 91 |
+
<div class="card">
|
| 92 |
+
<h2>Téléchargements</h2>
|
| 93 |
+
<div style="display: flex; gap: 1rem; flex-wrap: wrap;">
|
| 94 |
+
{% if run.pdf_file %}
|
| 95 |
+
<a href="{{ run.pdf_file.url }}" class="btn btn-secondary" download>📄 Rapport PDF</a>
|
| 96 |
+
{% endif %}
|
| 97 |
+
{% if run.csv_file %}
|
| 98 |
+
<a href="{{ run.csv_file.url }}" class="btn btn-secondary" download>📊 Résultats CSV</a>
|
| 99 |
+
{% endif %}
|
| 100 |
+
{% if run.plot_file %}
|
| 101 |
+
<a href="{{ run.plot_file.url }}" class="btn btn-secondary" download>📈 Graphique PNG</a>
|
| 102 |
+
{% endif %}
|
| 103 |
+
</div>
|
| 104 |
+
</div>
|
| 105 |
+
{% endif %}
|
| 106 |
+
|
| 107 |
+
{% if run.logs %}
|
| 108 |
+
<div class="card">
|
| 109 |
+
<h2>Logs d'exécution</h2>
|
| 110 |
+
<div class="logs">{{ run.logs }}</div>
|
| 111 |
+
</div>
|
| 112 |
+
{% endif %}
|
| 113 |
+
|
| 114 |
+
<div class="card">
|
| 115 |
+
<h2>Actions</h2>
|
| 116 |
+
{% if run.status == 'PENDING' or run.status == 'RUNNING' %}
|
| 117 |
+
<a href="{% url 'run_cancel' run.id %}" class="btn btn-danger">Annuler</a>
|
| 118 |
+
{% endif %}
|
| 119 |
+
{% if run.status == 'FAILURE' or run.status == 'CANCELLED' %}
|
| 120 |
+
<a href="{% url 'run_retry' run.id %}" class="btn btn-success">Réessayer</a>
|
| 121 |
+
{% endif %}
|
| 122 |
+
<a href="{% url 'run_list' %}" class="btn btn-secondary">Retour à la liste</a>
|
| 123 |
+
</div>
|
| 124 |
+
|
| 125 |
+
<script>
|
| 126 |
+
const runId = {{ run.id }};
|
| 127 |
+
const runStatus = "{{ run.status }}";
|
| 128 |
+
|
| 129 |
+
function updateStatus() {
|
| 130 |
+
fetch(`/run/${runId}/status/`)
|
| 131 |
+
.then(response => response.json())
|
| 132 |
+
.then(data => {
|
| 133 |
+
document.getElementById('status').textContent = getStatusText(data.status);
|
| 134 |
+
document.getElementById('status').className = `status-${data.status.toLowerCase()}`;
|
| 135 |
+
document.getElementById('progress').textContent = data.progress;
|
| 136 |
+
document.getElementById('progress-fill').style.width = `${data.progress}%`;
|
| 137 |
+
|
| 138 |
+
const resultsSection = document.getElementById('results-section');
|
| 139 |
+
const errorSection = document.getElementById('error-section');
|
| 140 |
+
const loadingIndicator = document.getElementById('loading-indicator');
|
| 141 |
+
|
| 142 |
+
if (data.status === 'RUNNING') {
|
| 143 |
+
loadingIndicator.style.display = 'block';
|
| 144 |
+
resultsSection.style.display = 'none';
|
| 145 |
+
errorSection.style.display = 'none';
|
| 146 |
+
setTimeout(updateStatus, 1000);
|
| 147 |
+
} else if (data.status === 'PENDING') {
|
| 148 |
+
loadingIndicator.style.display = 'block';
|
| 149 |
+
setTimeout(updateStatus, 1000);
|
| 150 |
+
} else if (data.status === 'SUCCESS') {
|
| 151 |
+
loadingIndicator.style.display = 'none';
|
| 152 |
+
resultsSection.style.display = 'block';
|
| 153 |
+
errorSection.style.display = 'none';
|
| 154 |
+
document.getElementById('status').textContent = 'Terminé';
|
| 155 |
+
location.reload();
|
| 156 |
+
} else if (data.status === 'FAILURE') {
|
| 157 |
+
loadingIndicator.style.display = 'none';
|
| 158 |
+
resultsSection.style.display = 'none';
|
| 159 |
+
errorSection.style.display = 'block';
|
| 160 |
+
document.getElementById('error-message').textContent = data.error_message || 'Erreur inconnue';
|
| 161 |
+
document.getElementById('status').textContent = 'Échoué';
|
| 162 |
+
} else if (data.status === 'CANCELLED') {
|
| 163 |
+
loadingIndicator.style.display = 'none';
|
| 164 |
+
document.getElementById('status').textContent = 'Annulé';
|
| 165 |
+
}
|
| 166 |
+
})
|
| 167 |
+
.catch(error => {
|
| 168 |
+
console.error('Erreur:', error);
|
| 169 |
+
setTimeout(updateStatus, 2000);
|
| 170 |
+
});
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
function getStatusText(status) {
|
| 174 |
+
const statusMap = {
|
| 175 |
+
'PENDING': 'En attente',
|
| 176 |
+
'RUNNING': 'En cours',
|
| 177 |
+
'SUCCESS': 'Terminé',
|
| 178 |
+
'FAILURE': 'Échoué',
|
| 179 |
+
'CANCELLED': 'Annulé'
|
| 180 |
+
};
|
| 181 |
+
return statusMap[status] || status;
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
if (runStatus === 'PENDING' || runStatus === 'RUNNING') {
|
| 185 |
+
updateStatus();
|
| 186 |
+
} else if (runStatus === 'SUCCESS') {
|
| 187 |
+
document.getElementById('results-section').style.display = 'block';
|
| 188 |
+
} else if (runStatus === 'FAILURE') {
|
| 189 |
+
document.getElementById('error-section').style.display = 'block';
|
| 190 |
+
document.getElementById('error-message').textContent = "{{ run.error_message|escapejs }}";
|
| 191 |
+
}
|
| 192 |
+
</script>
|
| 193 |
+
{% endblock %}
|
simulations/templates/simulations/run_list.html
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'simulations/base.html' %}
|
| 2 |
+
{% block title %}Historique des Simulations{% endblock %}
|
| 3 |
+
{% block content %}
|
| 4 |
+
<h1>Historique des Simulations</h1>
|
| 5 |
+
|
| 6 |
+
<div class="card">
|
| 7 |
+
<form method="get" style="display: flex; gap: 1rem; align-items: center;">
|
| 8 |
+
<label for="status">Filtrer par statut:</label>
|
| 9 |
+
<select name="status" id="status" onchange="this.form.submit()">
|
| 10 |
+
<option value="">Tous</option>
|
| 11 |
+
<option value="PENDING" {% if status_filter == 'PENDING' %}selected{% endif %}>En attente</option>
|
| 12 |
+
<option value="RUNNING" {% if status_filter == 'RUNNING' %}selected{% endif %}>En cours</option>
|
| 13 |
+
<option value="SUCCESS" {% if status_filter == 'SUCCESS' %}selected{% endif %}>Terminé</option>
|
| 14 |
+
<option value="FAILURE" {% if status_filter == 'FAILURE' %}selected{% endif %}>Échoué</option>
|
| 15 |
+
<option value="CANCELLED" {% if status_filter == 'CANCELLED' %}selected{% endif %}>Annulé</option>
|
| 16 |
+
</select>
|
| 17 |
+
</form>
|
| 18 |
+
</div>
|
| 19 |
+
|
| 20 |
+
<table>
|
| 21 |
+
<thead>
|
| 22 |
+
<tr>
|
| 23 |
+
<th>ID</th>
|
| 24 |
+
<th>Méthode</th>
|
| 25 |
+
<th>Nom</th>
|
| 26 |
+
<th>Statut</th>
|
| 27 |
+
<th>Progrès</th>
|
| 28 |
+
<th>Créé le</th>
|
| 29 |
+
<th>Actions</th>
|
| 30 |
+
</tr>
|
| 31 |
+
</thead>
|
| 32 |
+
<tbody>
|
| 33 |
+
{% for run in runs %}
|
| 34 |
+
<tr>
|
| 35 |
+
<td><a href="{% url 'run_detail' run.id %}">#{{ run.id }}</a></td>
|
| 36 |
+
<td>{{ run.method.name }}</td>
|
| 37 |
+
<td>{{ run.name|default:"-" }}</td>
|
| 38 |
+
<td class="status-{{ run.status|lower }}">{{ run.get_status_display }}</td>
|
| 39 |
+
<td>{{ run.progress }}%</td>
|
| 40 |
+
<td>{{ run.created_at|date:"d/m/Y H:i" }}</td>
|
| 41 |
+
<td>
|
| 42 |
+
<a href="{% url 'run_detail' run.id %}" class="btn" style="padding: 0.25rem 0.5rem;">Voir</a>
|
| 43 |
+
</td>
|
| 44 |
+
</tr>
|
| 45 |
+
{% empty %}
|
| 46 |
+
<tr>
|
| 47 |
+
<td colspan="7">Aucune simulation trouvée.</td>
|
| 48 |
+
</tr>
|
| 49 |
+
{% endfor %}
|
| 50 |
+
</tbody>
|
| 51 |
+
</table>
|
| 52 |
+
{% endblock %}
|
simulations/templatetags/__init__.py
ADDED
|
File without changes
|
simulations/templatetags/simulation_extras.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json as json_module
|
| 2 |
+
from django import template
|
| 3 |
+
|
| 4 |
+
register = template.Library()
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@register.filter
|
| 8 |
+
def json_format(value):
|
| 9 |
+
"""Affiche un objet Python au format JSON."""
|
| 10 |
+
return json_module.dumps(value, indent=2, ensure_ascii=False)
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
@register.filter
|
| 14 |
+
def multiply(value, arg):
|
| 15 |
+
"""Multiplie la valeur par arg."""
|
| 16 |
+
try:
|
| 17 |
+
return float(value) * float(arg)
|
| 18 |
+
except (ValueError, TypeError):
|
| 19 |
+
return 0
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
@register.filter
|
| 23 |
+
def divide(value, arg):
|
| 24 |
+
"""Divise la valeur par arg."""
|
| 25 |
+
try:
|
| 26 |
+
return float(value) / float(arg)
|
| 27 |
+
except (ValueError, TypeError, ZeroDivisionError):
|
| 28 |
+
return 0
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
@register.filter
|
| 32 |
+
def add(value, arg):
|
| 33 |
+
"""Ajoute arg à la valeur."""
|
| 34 |
+
try:
|
| 35 |
+
return float(value) + float(arg)
|
| 36 |
+
except (ValueError, TypeError):
|
| 37 |
+
return value
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@register.filter
|
| 41 |
+
def scientific(value):
|
| 42 |
+
"""Affiche en notation scientifique."""
|
| 43 |
+
try:
|
| 44 |
+
return f"{float(value):.2e}"
|
| 45 |
+
except (ValueError, TypeError):
|
| 46 |
+
return value
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@register.filter
|
| 50 |
+
def sigfig(value, digits=2):
|
| 51 |
+
"""Affiche avec un nombre de chiffres significatifs."""
|
| 52 |
+
try:
|
| 53 |
+
val = float(value)
|
| 54 |
+
if val == 0:
|
| 55 |
+
return "0"
|
| 56 |
+
import math
|
| 57 |
+
magnitude = math.floor(math.log10(abs(val)))
|
| 58 |
+
precision = digits - 1 - magnitude
|
| 59 |
+
return f"{val:.{max(1, precision)}f}"
|
| 60 |
+
except (ValueError, TypeError):
|
| 61 |
+
return value
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@register.filter
|
| 65 |
+
def is_list(value):
|
| 66 |
+
"""Vérifie si la valeur est une liste."""
|
| 67 |
+
return isinstance(value, list)
|
| 68 |
+
|
| 69 |
+
|
| 70 |
+
@register.filter
|
| 71 |
+
def is_dict(value):
|
| 72 |
+
"""Vérifie si la valeur est un dictionnaire."""
|
| 73 |
+
return isinstance(value, dict)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@register.filter
|
| 77 |
+
def contains(value, arg):
|
| 78 |
+
"""Vérifie si la valeur contient arg."""
|
| 79 |
+
return arg in str(value)
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
@register.filter
|
| 83 |
+
def length(value):
|
| 84 |
+
"""Retourne la longueur d'une liste."""
|
| 85 |
+
try:
|
| 86 |
+
return len(value)
|
| 87 |
+
except (TypeError, ValueError):
|
| 88 |
+
return 0
|
simulations/urls.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import path, include
|
| 2 |
+
from rest_framework.routers import DefaultRouter
|
| 3 |
+
from .views import SimulationMethodViewSet, SimulationRunViewSet
|
| 4 |
+
|
| 5 |
+
router = DefaultRouter()
|
| 6 |
+
router.register(r'methods', SimulationMethodViewSet, basename='method')
|
| 7 |
+
router.register(r'runs', SimulationRunViewSet, basename='run')
|
| 8 |
+
|
| 9 |
+
urlpatterns = [
|
| 10 |
+
path('', include(router.urls)),
|
| 11 |
+
]
|
simulations/urls_views.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.urls import path
|
| 2 |
+
from . import views_views
|
| 3 |
+
|
| 4 |
+
urlpatterns = [
|
| 5 |
+
path('', views_views.home, name='home'),
|
| 6 |
+
path('methods/', views_views.method_list, name='method_list'),
|
| 7 |
+
path('methods/<slug:slug>/', views_views.method_detail, name='method_detail'),
|
| 8 |
+
path('run/create/', views_views.run_create, name='run_create'),
|
| 9 |
+
path('run/create/<slug:method_slug>/', views_views.run_create, name='run_create_method'),
|
| 10 |
+
path('run/<int:run_id>/', views_views.run_detail, name='run_detail'),
|
| 11 |
+
path('run/<int:run_id>/status/', views_views.run_status, name='run_status'),
|
| 12 |
+
path('run/<int:run_id>/cancel/', views_views.run_cancel, name='run_cancel'),
|
| 13 |
+
path('run/<int:run_id>/retry/', views_views.run_retry, name='run_retry'),
|
| 14 |
+
path('runs/', views_views.run_list, name='run_list'),
|
| 15 |
+
]
|
simulations/views.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from rest_framework import viewsets, status
|
| 2 |
+
from rest_framework.decorators import action
|
| 3 |
+
from rest_framework.response import Response
|
| 4 |
+
from .models import SimulationMethod, SimulationRun
|
| 5 |
+
from .serializers import (
|
| 6 |
+
SimulationMethodSerializer,
|
| 7 |
+
SimulationRunSerializer,
|
| 8 |
+
SimulationRunCreateSerializer,
|
| 9 |
+
)
|
| 10 |
+
from .tasks import run_simulation
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class SimulationMethodViewSet(viewsets.ModelViewSet):
|
| 14 |
+
queryset = SimulationMethod.objects.filter(is_active=True)
|
| 15 |
+
serializer_class = SimulationMethodSerializer
|
| 16 |
+
lookup_field = 'slug'
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class SimulationRunViewSet(viewsets.ModelViewSet):
|
| 20 |
+
queryset = SimulationRun.objects.all()
|
| 21 |
+
|
| 22 |
+
def get_serializer_class(self):
|
| 23 |
+
if self.action == 'create':
|
| 24 |
+
return SimulationRunCreateSerializer
|
| 25 |
+
if self.action == 'retrieve':
|
| 26 |
+
return SimulationRunDetailSerializer
|
| 27 |
+
return SimulationRunSerializer
|
| 28 |
+
|
| 29 |
+
def create(self, request, *args, **kwargs):
|
| 30 |
+
serializer = self.get_serializer(data=request.data)
|
| 31 |
+
serializer.is_valid(raise_exception=True)
|
| 32 |
+
|
| 33 |
+
run = serializer.save()
|
| 34 |
+
|
| 35 |
+
run_simulation.delay(run.id)
|
| 36 |
+
|
| 37 |
+
output_serializer = SimulationRunSerializer(run)
|
| 38 |
+
return Response(
|
| 39 |
+
output_serializer.data,
|
| 40 |
+
status=status.HTTP_202_ACCEPTED
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
@action(detail=True, methods=['post'])
|
| 44 |
+
def cancel(self, request, pk=None):
|
| 45 |
+
run = self.get_object()
|
| 46 |
+
if run.status in ['PENDING', 'RUNNING']:
|
| 47 |
+
run.status = 'CANCELLED'
|
| 48 |
+
run.save(update_fields=['status'])
|
| 49 |
+
return Response({'status': 'cancelled'})
|
| 50 |
+
return Response(
|
| 51 |
+
{'error': 'Cannot cancel run in current state'},
|
| 52 |
+
status=status.HTTP_400_BAD_REQUEST
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
@action(detail=True, methods=['get'])
|
| 56 |
+
def status(self, request, pk=None):
|
| 57 |
+
run = self.get_object()
|
| 58 |
+
return Response({
|
| 59 |
+
'id': run.id,
|
| 60 |
+
'status': run.status,
|
| 61 |
+
'progress': run.progress,
|
| 62 |
+
'error_message': run.error_message,
|
| 63 |
+
'created_at': run.created_at,
|
| 64 |
+
'started_at': run.started_at,
|
| 65 |
+
'completed_at': run.completed_at,
|
| 66 |
+
})
|
| 67 |
+
|
| 68 |
+
@action(detail=False, methods=['get'])
|
| 69 |
+
def pending(self, request):
|
| 70 |
+
runs = SimulationRun.objects.filter(status='PENDING')
|
| 71 |
+
serializer = self.get_serializer(runs, many=True)
|
| 72 |
+
return Response(serializer.data)
|
| 73 |
+
|
| 74 |
+
@action(detail=False, methods=['get'])
|
| 75 |
+
def running(self, request):
|
| 76 |
+
runs = SimulationRun.objects.filter(status='RUNNING')
|
| 77 |
+
serializer = self.get_serializer(runs, many=True)
|
| 78 |
+
return Response(serializer.data)
|
| 79 |
+
|
| 80 |
+
@action(detail=False, methods=['get'])
|
| 81 |
+
def recent(self, request):
|
| 82 |
+
limit = int(request.query_params.get('limit', 10))
|
| 83 |
+
runs = SimulationRun.objects.all()[:limit]
|
| 84 |
+
serializer = self.get_serializer(runs, many=True)
|
| 85 |
+
return Response(serializer.data)
|
simulations/views_views.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from django.shortcuts import render, get_object_or_404, redirect
|
| 2 |
+
from django.http import JsonResponse
|
| 3 |
+
from .models import SimulationMethod, SimulationRun
|
| 4 |
+
from .tasks import run_simulation
|
| 5 |
+
import json
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def home(request):
|
| 9 |
+
methods = SimulationMethod.objects.filter(is_active=True)
|
| 10 |
+
recent_runs = SimulationRun.objects.all()[:5]
|
| 11 |
+
return render(request, 'simulations/home.html', {
|
| 12 |
+
'methods': methods,
|
| 13 |
+
'recent_runs': recent_runs,
|
| 14 |
+
})
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def method_list(request):
|
| 18 |
+
methods = SimulationMethod.objects.filter(is_active=True)
|
| 19 |
+
return render(request, 'simulations/method_list.html', {
|
| 20 |
+
'methods': methods,
|
| 21 |
+
})
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def method_detail(request, slug):
|
| 25 |
+
method = get_object_or_404(SimulationMethod, slug=slug, is_active=True)
|
| 26 |
+
return render(request, 'simulations/method_detail.html', {
|
| 27 |
+
'method': method,
|
| 28 |
+
})
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def run_create(request, method_slug=None):
|
| 32 |
+
methods = SimulationMethod.objects.filter(is_active=True)
|
| 33 |
+
|
| 34 |
+
if method_slug:
|
| 35 |
+
selected_method = get_object_or_404(
|
| 36 |
+
SimulationMethod, slug=method_slug, is_active=True
|
| 37 |
+
)
|
| 38 |
+
else:
|
| 39 |
+
selected_method = None
|
| 40 |
+
|
| 41 |
+
if request.method == 'POST':
|
| 42 |
+
method_id = request.POST.get('method')
|
| 43 |
+
parameters = json.loads(request.POST.get('parameters', '{}'))
|
| 44 |
+
name = request.POST.get('name', '')
|
| 45 |
+
|
| 46 |
+
method = get_object_or_404(SimulationMethod, pk=method_id)
|
| 47 |
+
|
| 48 |
+
run = SimulationRun.objects.create(
|
| 49 |
+
method=method,
|
| 50 |
+
name=name,
|
| 51 |
+
parameters=parameters,
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
run_simulation.delay(run.id)
|
| 55 |
+
|
| 56 |
+
return redirect('run_detail', run_id=run.id)
|
| 57 |
+
|
| 58 |
+
return render(request, 'simulations/run_create.html', {
|
| 59 |
+
'methods': methods,
|
| 60 |
+
'selected_method': selected_method,
|
| 61 |
+
})
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def run_detail(request, run_id):
|
| 65 |
+
run = get_object_or_404(SimulationRun, pk=run_id)
|
| 66 |
+
return render(request, 'simulations/run_detail.html', {
|
| 67 |
+
'run': run,
|
| 68 |
+
})
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def run_list(request):
|
| 72 |
+
runs = SimulationRun.objects.all()
|
| 73 |
+
status_filter = request.GET.get('status')
|
| 74 |
+
if status_filter:
|
| 75 |
+
runs = runs.filter(status=status_filter)
|
| 76 |
+
return render(request, 'simulations/run_list.html', {
|
| 77 |
+
'runs': runs,
|
| 78 |
+
'status_filter': status_filter,
|
| 79 |
+
})
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def run_status(request, run_id):
|
| 83 |
+
run = get_object_or_404(SimulationRun, pk=run_id)
|
| 84 |
+
return JsonResponse({
|
| 85 |
+
'id': run.id,
|
| 86 |
+
'status': run.status,
|
| 87 |
+
'progress': run.progress,
|
| 88 |
+
'error_message': run.error_message,
|
| 89 |
+
'started_at': run.started_at.strftime('%Y-%m-%d %H:%M:%S') if run.started_at else None,
|
| 90 |
+
'completed_at': run.completed_at.strftime('%Y-%m-%d %H:%M:%S') if run.completed_at else None,
|
| 91 |
+
})
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
def run_cancel(request, run_id):
|
| 95 |
+
run = get_object_or_404(SimulationRun, pk=run_id)
|
| 96 |
+
if run.status in ['PENDING', 'RUNNING']:
|
| 97 |
+
run.status = 'CANCELLED'
|
| 98 |
+
run.save(update_fields=['status'])
|
| 99 |
+
return redirect('run_detail', run_id=run.id)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def run_retry(request, run_id):
|
| 103 |
+
run = get_object_or_404(SimulationRun, pk=run_id)
|
| 104 |
+
run.set_pending()
|
| 105 |
+
run_simulation.delay(run.id)
|
| 106 |
+
return redirect('run_detail', run_id=run.id)
|
simulationserver/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Simulation Numérique Server
|
| 2 |
+
|
| 3 |
+
Plateforme Django pour exécuter des méthodes de simulation numérique avec traitement asynchrone via Celery et Redis.
|
| 4 |
+
|
| 5 |
+
## Architecture
|
| 6 |
+
|
| 7 |
+
```
|
| 8 |
+
simulationserver/
|
| 9 |
+
├── simulations/ # App Django
|
| 10 |
+
│ ├── models.py # Modèles (SimulationMethod, SimulationRun)
|
| 11 |
+
│ ├── tasks.py # Tâches Celery
|
| 12 |
+
│ ├── views.py # API REST
|
| 13 |
+
│ ├── views_views.py # Vues HTML
|
| 14 |
+
│ └── templates/ # Templates
|
| 15 |
+
├── simulationserver/ # Configuration Django
|
| 16 |
+
│ ├── settings.py # Settings + Celery config
|
| 17 |
+
│ ├── celery.py # Configuration Celery
|
| 18 |
+
│ └── urls.py # URLs
|
| 19 |
+
└── manage.py
|
| 20 |
+
```
|
| 21 |
+
|
| 22 |
+
## Installation
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
pip install -r simulationserver/requirements.txt
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## Configuration
|
| 29 |
+
|
| 30 |
+
1. Installer et démarrer Redis:
|
| 31 |
+
```bash
|
| 32 |
+
# Sur Debian/Ubuntu
|
| 33 |
+
sudo apt install redis-server
|
| 34 |
+
sudo systemctl start redis
|
| 35 |
+
|
| 36 |
+
# Ou avec Docker
|
| 37 |
+
docker run -d -p 6379:6379 redis:alpine
|
| 38 |
+
```
|
| 39 |
+
|
| 40 |
+
## Démarrage
|
| 41 |
+
|
| 42 |
+
1. Appliquer les migrations:
|
| 43 |
+
```bash
|
| 44 |
+
python manage.py migrate
|
| 45 |
+
```
|
| 46 |
+
|
| 47 |
+
2. Initialiser les méthodes de simulation:
|
| 48 |
+
```bash
|
| 49 |
+
python manage.py init_simulation_methods
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
3. Démarrer le serveur Django:
|
| 53 |
+
```bash
|
| 54 |
+
python manage.py runserver 0.0.0.0:8000
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
4. Démarrer le worker Celery (dans un autre terminal):
|
| 58 |
+
```bash
|
| 59 |
+
cd /root/bookshop
|
| 60 |
+
celery -A simulationserver worker --loglevel=info
|
| 61 |
+
```
|
| 62 |
+
|
| 63 |
+
## Utilisation
|
| 64 |
+
|
| 65 |
+
### Interface Web
|
| 66 |
+
|
| 67 |
+
- **Accueil**: http://localhost:8000/
|
| 68 |
+
- **Liste des méthodes**: http://localhost:8000/methods/
|
| 69 |
+
- **Créer une simulation**: http://localhost:8000/run/create/
|
| 70 |
+
- **Historique**: http://localhost:8000/runs/
|
| 71 |
+
|
| 72 |
+
### API REST
|
| 73 |
+
|
| 74 |
+
- `GET /api/methods/` - Liste des méthodes
|
| 75 |
+
- `GET /api/methods/{slug}/` - Détail d'une méthode
|
| 76 |
+
- `POST /api/runs/` - Créer et lancer une simulation
|
| 77 |
+
- `GET /api/runs/{id}/` - Détail d'une simulation
|
| 78 |
+
- `GET /api/runs/{id}/status/` - Statut d'une simulation
|
| 79 |
+
- `POST /api/runs/{id}/cancel/` - Annuler une simulation
|
| 80 |
+
- `GET /api/runs/recent/` - Simulations récentes
|
| 81 |
+
|
| 82 |
+
## Modèles
|
| 83 |
+
|
| 84 |
+
### SimulationMethod
|
| 85 |
+
|
| 86 |
+
Décrit une méthode de simulation:
|
| 87 |
+
- `name`: Nom de la méthode
|
| 88 |
+
- `slug`: Identifiant unique
|
| 89 |
+
- `description`: Description courte
|
| 90 |
+
- `theory`: Documentation/theorie
|
| 91 |
+
- `parameters_schema`: Schema JSON des paramètres
|
| 92 |
+
- `default_parameters`: Paramètres par défaut
|
| 93 |
+
|
| 94 |
+
### SimulationRun
|
| 95 |
+
|
| 96 |
+
Une exécution de simulation:
|
| 97 |
+
- `method`: Méthode utilisée
|
| 98 |
+
- `parameters`: Paramètres de la simulation
|
| 99 |
+
- `status`: PENDING, RUNNING, SUCCESS, FAILURE, CANCELLED
|
| 100 |
+
- `progress`: Pourcentage de progression
|
| 101 |
+
- `logs`: Logs d'exécution
|
| 102 |
+
- `result_data`: Résultats JSON
|
| 103 |
+
|
| 104 |
+
## Tâches Celery
|
| 105 |
+
|
| 106 |
+
Les simulations sont exécutées en arrière-plan via Celery:
|
| 107 |
+
- Le worker lit les paramètres depuis la base
|
| 108 |
+
- Exécute le calcul
|
| 109 |
+
- Met à jour le statut et les résultats
|
| 110 |
+
- Les pages web font du polling pour le suivi
|
| 111 |
+
|
| 112 |
+
## Ajouter une nouvelle méthode
|
| 113 |
+
|
| 114 |
+
1. Ajouter la fonction de simulation dans `simulations/tasks.py`
|
| 115 |
+
2. Ajouter le mapping dans `SIMULATION_METHODS`
|
| 116 |
+
3. Créer un `SimulationMethod` via l'admin ou `init_simulation_methods`
|
simulationserver/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from .celery import app as celery_app
|
| 2 |
+
|
| 3 |
+
__all__ = ('celery_app',)
|
simulationserver/asgi.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
ASGI config for simulationserver project.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from django.core.asgi import get_asgi_application
|
| 7 |
+
|
| 8 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'simulationserver.settings')
|
| 9 |
+
application = get_asgi_application()
|
simulationserver/celery.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
from celery import Celery
|
| 3 |
+
|
| 4 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'simulationserver.settings')
|
| 5 |
+
|
| 6 |
+
app = Celery('simulationserver')
|
| 7 |
+
app.config_from_object('django.conf:settings', namespace='CELERY')
|
| 8 |
+
app.autodiscover_tasks()
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
@app.task(bind=True, ignore_result=True)
|
| 12 |
+
def debug_task(self):
|
| 13 |
+
print(f'Request: {self.request!r}')
|
simulationserver/requirements.txt
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Django>=4.2,<5.0
|
| 2 |
+
djangorestframework>=3.14,<4.0
|
| 3 |
+
celery>=5.3,<6.0
|
| 4 |
+
redis>=4.5,<6.0
|
| 5 |
+
numpy>=1.24,<2.0
|
| 6 |
+
matplotlib>=3.7,<4.0
|
| 7 |
+
reportlab>=4.0,<5.0
|
simulationserver/settings.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Django settings for simulationserver project.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
import os
|
| 7 |
+
import sys
|
| 8 |
+
from celery import Celery
|
| 9 |
+
|
| 10 |
+
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
| 11 |
+
BASE_DIR = Path(__file__).resolve().parent.parent
|
| 12 |
+
|
| 13 |
+
# Add django-polls to path for the existing app
|
| 14 |
+
sys.path.insert(0, str(Path.home() / 'django-polls'))
|
| 15 |
+
|
| 16 |
+
# Quick-start development settings
|
| 17 |
+
SECRET_KEY = os.environ.get('DJANGO_SECRET_KEY', 'django-insecure-simulationserver-key-change-in-production')
|
| 18 |
+
DEBUG = os.environ.get('DEBUG', 'True').lower() in ('true', '1', 'yes')
|
| 19 |
+
ALLOWED_HOSTS = ['*']
|
| 20 |
+
|
| 21 |
+
# Application definition
|
| 22 |
+
INSTALLED_APPS = [
|
| 23 |
+
'django.contrib.admin',
|
| 24 |
+
'django.contrib.auth',
|
| 25 |
+
'django.contrib.contenttypes',
|
| 26 |
+
'django.contrib.sessions',
|
| 27 |
+
'django.contrib.messages',
|
| 28 |
+
'django.contrib.staticfiles',
|
| 29 |
+
'rest_framework',
|
| 30 |
+
'simulations',
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
MIDDLEWARE = [
|
| 34 |
+
'django.middleware.security.SecurityMiddleware',
|
| 35 |
+
'django.contrib.sessions.middleware.SessionMiddleware',
|
| 36 |
+
'django.middleware.common.CommonMiddleware',
|
| 37 |
+
'django.middleware.csrf.CsrfViewMiddleware',
|
| 38 |
+
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
| 39 |
+
'django.contrib.messages.middleware.MessageMiddleware',
|
| 40 |
+
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
| 41 |
+
]
|
| 42 |
+
|
| 43 |
+
ROOT_URLCONF = 'simulationserver.urls'
|
| 44 |
+
|
| 45 |
+
TEMPLATES = [
|
| 46 |
+
{
|
| 47 |
+
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
| 48 |
+
'DIRS': [BASE_DIR / 'templates'],
|
| 49 |
+
'APP_DIRS': True,
|
| 50 |
+
'OPTIONS': {
|
| 51 |
+
'context_processors': [
|
| 52 |
+
'django.template.context_processors.debug',
|
| 53 |
+
'django.template.context_processors.request',
|
| 54 |
+
'django.contrib.auth.context_processors.auth',
|
| 55 |
+
'django.contrib.messages.context_processors.messages',
|
| 56 |
+
],
|
| 57 |
+
},
|
| 58 |
+
},
|
| 59 |
+
]
|
| 60 |
+
|
| 61 |
+
WSGI_APPLICATION = 'simulationserver.wsgi.application'
|
| 62 |
+
|
| 63 |
+
# Database
|
| 64 |
+
DATABASES = {
|
| 65 |
+
'default': {
|
| 66 |
+
'ENGINE': 'django.db.backends.sqlite3',
|
| 67 |
+
'NAME': BASE_DIR / 'db.sqlite3',
|
| 68 |
+
}
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
# Celery Configuration
|
| 72 |
+
CELERY_BROKER_URL = os.environ.get('CELERY_BROKER_URL', 'redis://localhost:6379/0')
|
| 73 |
+
CELERY_RESULT_BACKEND = os.environ.get('CELERY_RESULT_BACKEND', 'redis://localhost:6379/0')
|
| 74 |
+
CELERY_ACCEPT_CONTENT = ['json']
|
| 75 |
+
CELERY_TASK_SERIALIZER = 'json'
|
| 76 |
+
CELERY_RESULT_SERIALIZER = 'json'
|
| 77 |
+
CELERY_TIMEZONE = 'UTC'
|
| 78 |
+
|
| 79 |
+
# REST Framework
|
| 80 |
+
REST_FRAMEWORK = {
|
| 81 |
+
'DEFAULT_RENDERER_CLASSES': [
|
| 82 |
+
'rest_framework.renderers.JSONRenderer',
|
| 83 |
+
],
|
| 84 |
+
'DEFAULT_PARSER_CLASSES': [
|
| 85 |
+
'rest_framework.parsers.JSONParser',
|
| 86 |
+
'rest_framework.parsers.MultiPartParser',
|
| 87 |
+
],
|
| 88 |
+
}
|
| 89 |
+
|
| 90 |
+
# Password validation
|
| 91 |
+
AUTH_PASSWORD_VALIDATORS = [
|
| 92 |
+
{'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'},
|
| 93 |
+
{'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'},
|
| 94 |
+
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
|
| 95 |
+
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
| 96 |
+
]
|
| 97 |
+
|
| 98 |
+
# Internationalization
|
| 99 |
+
LANGUAGE_CODE = 'fr'
|
| 100 |
+
TIME_ZONE = 'UTC'
|
| 101 |
+
USE_I18N = True
|
| 102 |
+
USE_TZ = True
|
| 103 |
+
|
| 104 |
+
# Static files
|
| 105 |
+
STATIC_URL = 'static/'
|
| 106 |
+
|
| 107 |
+
# Media files
|
| 108 |
+
MEDIA_URL = 'media/'
|
| 109 |
+
MEDIA_ROOT = BASE_DIR / 'media'
|
| 110 |
+
|
| 111 |
+
# Default primary key field type
|
| 112 |
+
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
simulationserver/templates/simulations/home.html
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends 'simulations/base.html' %}
|
| 2 |
+
{% load simulation_extras %}
|
| 3 |
+
{% block title %}Simulations Numériques{% endblock %}
|
| 4 |
+
{% block content %}
|
| 5 |
+
<h1>🧮 Simulations Numériques</h1>
|
| 6 |
+
<p>Plateforme d'exécution de méthodes de simulation numérique avec traitement asynchrone.</p>
|
| 7 |
+
|
| 8 |
+
<h2>Méthodes disponibles</h2>
|
| 9 |
+
<div class="grid">
|
| 10 |
+
{% for method in methods %}
|
| 11 |
+
<div class="card method-card" style="position: relative;">
|
| 12 |
+
<div style="position: absolute; top: 10px; right: 10px; font-size: 2em;">
|
| 13 |
+
{% if 'monte-carlo' in method.slug %}🎲
|
| 14 |
+
{% elif 'diffusion' in method.slug %}🌡️
|
| 15 |
+
{% elif 'linear' in method.slug %}📐
|
| 16 |
+
{% elif 'heat' in method.slug %}🔥
|
| 17 |
+
{% elif 'traffic' in method.slug %}🚗
|
| 18 |
+
{% elif 'phugoid' in method.slug %}✈️
|
| 19 |
+
{% elif 'elasticity' in method.slug %}⚙️
|
| 20 |
+
{% elif 'wave' in method.slug %}🌊
|
| 21 |
+
{% else %}📊{% endif %}
|
| 22 |
+
</div>
|
| 23 |
+
<h3>{{ method.name }}</h3>
|
| 24 |
+
<p>{{ method.description }}</p>
|
| 25 |
+
<div style="margin-top: 1rem;">
|
| 26 |
+
<a href="{% url 'method_detail' method.slug %}" class="btn btn-secondary">En savoir plus</a>
|
| 27 |
+
<a href="{% url 'run_create_method' method.slug %}" class="btn btn-success">Lancer une simulation</a>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
{% empty %}
|
| 31 |
+
<p>Aucune méthode disponible. Exécutez <code>python manage.py init_simulation_methods</code></p>
|
| 32 |
+
{% endfor %}
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
{% if recent_runs %}
|
| 36 |
+
<h2>Simulations récentes</h2>
|
| 37 |
+
<table>
|
| 38 |
+
<thead>
|
| 39 |
+
<tr>
|
| 40 |
+
<th>ID</th>
|
| 41 |
+
<th>Méthode</th>
|
| 42 |
+
<th>Statut</th>
|
| 43 |
+
<th>Progrès</th>
|
| 44 |
+
<th>Créé le</th>
|
| 45 |
+
<th>Actions</th>
|
| 46 |
+
</tr>
|
| 47 |
+
</thead>
|
| 48 |
+
<tbody>
|
| 49 |
+
{% for run in recent_runs %}
|
| 50 |
+
<tr>
|
| 51 |
+
<td><a href="{% url 'run_detail' run.id %}">#{{ run.id }}</a></td>
|
| 52 |
+
<td>{{ run.method.name }}</td>
|
| 53 |
+
<td class="status-{{ run.status|lower }}">{{ run.get_status_display }}</td>
|
| 54 |
+
<td>
|
| 55 |
+
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
| 56 |
+
<div class="progress-bar" style="width: 50px; height: 8px;">
|
| 57 |
+
<div class="progress-fill" style="width: {{ run.progress }}%;"></div>
|
| 58 |
+
</div>
|
| 59 |
+
<span>{{ run.progress }}%</span>
|
| 60 |
+
</div>
|
| 61 |
+
</td>
|
| 62 |
+
<td>{{ run.created_at|date:"d/m/Y H:i" }}</td>
|
| 63 |
+
<td>
|
| 64 |
+
<a href="{% url 'run_detail' run.id %}" class="btn" style="padding: 0.25rem 0.5rem; font-size: 0.8em;">Voir</a>
|
| 65 |
+
</td>
|
| 66 |
+
</tr>
|
| 67 |
+
{% endfor %}
|
| 68 |
+
</tbody>
|
| 69 |
+
</table>
|
| 70 |
+
{% endif %}
|
| 71 |
+
|
| 72 |
+
<div class="card" style="margin-top: 2rem;">
|
| 73 |
+
<h2>📖 Guide rapide</h2>
|
| 74 |
+
<ol>
|
| 75 |
+
<li>Choisissez une méthode de simulation dans la liste ci-dessus</li>
|
| 76 |
+
<li>Configurez les paramètres (ou laissez les valeurs par défaut)</li>
|
| 77 |
+
<li>Lancez la simulation - elle s'exécute en arrière-plan</li>
|
| 78 |
+
<li>Attendez que le statut passe à "Terminé"</li>
|
| 79 |
+
<li>Visualisez les résultats et téléchargez les fichiers (PNG, PDF, CSV)</li>
|
| 80 |
+
</ol>
|
| 81 |
+
|
| 82 |
+
<h3>Architecture</h3>
|
| 83 |
+
<ul>
|
| 84 |
+
<li><strong>Django</strong> : Interface Web + API REST</li>
|
| 85 |
+
<li><strong>Celery</strong> : Traitement asynchrone des simulations</li>
|
| 86 |
+
<li><strong>Redis</strong> : Message broker</li>
|
| 87 |
+
<li><strong>Matplotlib</strong> : Génération des graphiques</li>
|
| 88 |
+
<li><strong>ReportLab</strong> : Génération des rapports PDF</li>
|
| 89 |
+
</ul>
|
| 90 |
+
</div>
|
| 91 |
+
|
| 92 |
+
<div class="card" style="margin-top: 1rem;">
|
| 93 |
+
<h2>🔧 Ajouter une nouvelle méthode</h2>
|
| 94 |
+
<p>Pour ajouter une nouvelle méthode de simulation :</p>
|
| 95 |
+
<ol>
|
| 96 |
+
<li>Créer une fonction dans <code>simulations/tasks.py</code> qui retourne un générateur</li>
|
| 97 |
+
<li>Ajouter la méthode à <code>SIMULATION_METHODS</code> dans tasks.py</li>
|
| 98 |
+
<li>Créer un <code>SimulationMethod</code> via l'admin ou la commande <code>init_simulation_methods</code></li>
|
| 99 |
+
<li>Le système gérera automatiquement l'affichage et les téléchargements</li>
|
| 100 |
+
</ol>
|
| 101 |
+
|
| 102 |
+
<h3>Exemple de fonction de simulation</h3>
|
| 103 |
+
<pre style="background: #2c3e50; color: #ecf0f1; padding: 1rem; border-radius: 4px; overflow-x: auto;">
|
| 104 |
+
def ma_nouvelle_simulation(params):
|
| 105 |
+
"""Ma nouvelle simulation."""
|
| 106 |
+
# Code de simulation avec yield pour la progression
|
| 107 |
+
for i in range(100):
|
| 108 |
+
# Faire des calculs...
|
| 109 |
+
yield i # Progrès de 0 à 100
|
| 110 |
+
|
| 111 |
+
return {'resultat': 'valeur'}</pre>
|
| 112 |
+
</div>
|
| 113 |
+
{% endblock %}
|
simulationserver/urls.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
URL configuration for simulationserver project.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
from django.contrib import admin
|
| 6 |
+
from django.urls import path, include
|
| 7 |
+
from django.conf import settings
|
| 8 |
+
from django.conf.urls.static import static
|
| 9 |
+
|
| 10 |
+
urlpatterns = [
|
| 11 |
+
path('admin/', admin.site.urls),
|
| 12 |
+
path('api/', include('simulations.urls')),
|
| 13 |
+
path('', include('simulations.urls_views')),
|
| 14 |
+
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
|
simulationserver/wsgi.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
WSGI config for simulationserver project.
|
| 3 |
+
"""
|
| 4 |
+
|
| 5 |
+
import os
|
| 6 |
+
from django.core.wsgi import get_wsgi_application
|
| 7 |
+
|
| 8 |
+
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'simulationserver.settings')
|
| 9 |
+
application = get_wsgi_application()
|