tiffank1802 commited on
Commit
b42075e
·
1 Parent(s): 13b8d6c

Add Django simulation server with Docker config

Browse files
Files changed (50) hide show
  1. .dockerignore +37 -0
  2. Dockerfile +22 -0
  3. Dockerfile.hfspaces +23 -0
  4. HF_SPACES_README.md +132 -0
  5. README.md +4 -6
  6. bookshop_old/__init__.py +0 -0
  7. bookshop_old/asgi.py +16 -0
  8. bookshop_old/settings.py +128 -0
  9. bookshop_old/templates/admin/base_site.html +13 -0
  10. bookshop_old/templates/admin/index.html +36 -0
  11. bookshop_old/urls.py +25 -0
  12. bookshop_old/wsgi.py +16 -0
  13. docker-compose.yml +41 -0
  14. manage.py +22 -0
  15. requirements.txt +7 -0
  16. simulations/__init__.py +0 -0
  17. simulations/admin.py +22 -0
  18. simulations/apps.py +7 -0
  19. simulations/gradio_app.py +144 -0
  20. simulations/management/__init__.py +0 -0
  21. simulations/management/commands/__init__.py +0 -0
  22. simulations/management/commands/init_simulation_methods.py +205 -0
  23. simulations/migrations/0001_initial.py +61 -0
  24. simulations/migrations/__init__.py +0 -0
  25. simulations/models.py +515 -0
  26. simulations/registry.py +181 -0
  27. simulations/serializers.py +57 -0
  28. simulations/tasks.py +418 -0
  29. simulations/templates/simulations/base.html +78 -0
  30. simulations/templates/simulations/home.html +56 -0
  31. simulations/templates/simulations/method_detail.html +24 -0
  32. simulations/templates/simulations/method_list.html +17 -0
  33. simulations/templates/simulations/run_create.html +57 -0
  34. simulations/templates/simulations/run_detail.html +193 -0
  35. simulations/templates/simulations/run_list.html +52 -0
  36. simulations/templatetags/__init__.py +0 -0
  37. simulations/templatetags/simulation_extras.py +88 -0
  38. simulations/urls.py +11 -0
  39. simulations/urls_views.py +15 -0
  40. simulations/views.py +85 -0
  41. simulations/views_views.py +106 -0
  42. simulationserver/README.md +116 -0
  43. simulationserver/__init__.py +3 -0
  44. simulationserver/asgi.py +9 -0
  45. simulationserver/celery.py +13 -0
  46. simulationserver/requirements.txt +7 -0
  47. simulationserver/settings.py +112 -0
  48. simulationserver/templates/simulations/home.html +113 -0
  49. simulationserver/urls.py +14 -0
  50. 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: gradio
7
- sdk_version: 6.3.0
8
- app_file: app.py
9
  pinned: false
10
  license: mit
11
- short_description: simulations models
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
+ &gt; <a href="{% url 'admin:app_list' app_label='polls' %}">Polls</a>
11
+ &gt; {% 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()