forked from EHV/sejeteralo
modification calcul mediane
This commit is contained in:
@@ -5,12 +5,22 @@ Computes the element-wise median of (vinf, a, b, c, d, e) across all active vote
|
|||||||
This parametric median is chosen over geometric median because:
|
This parametric median is chosen over geometric median because:
|
||||||
- It's transparent and politically explainable
|
- It's transparent and politically explainable
|
||||||
- The result is itself a valid set of Bézier parameters
|
- The result is itself a valid set of Bézier parameters
|
||||||
|
|
||||||
|
Post-processing: the median is sanitized to guarantee monotonicity of tier 2.
|
||||||
|
The condition for tier 2 to be non-decreasing is e <= 1/3.
|
||||||
|
If the raw median violates this, e is clamped to E_MAX_MONOTONE.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
# Maximum value of e that guarantees tier-2 price monotonicity.
|
||||||
|
# prix_m3_2 = p0 + (pmax-p0) * ((1-3e)t³ + 3e t²)
|
||||||
|
# The cubic is monotone non-decreasing on [0,1] iff (1-3e) >= 0, i.e. e <= 1/3.
|
||||||
|
E_MAX_MONOTONE = 1.0 / 3.0
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class VoteParams:
|
class VoteParams:
|
||||||
"""The 6 citizen-adjustable parameters."""
|
"""The 6 citizen-adjustable parameters."""
|
||||||
@@ -22,11 +32,37 @@ class VoteParams:
|
|||||||
e: float
|
e: float
|
||||||
|
|
||||||
|
|
||||||
|
def _is_tier2_monotone(e: float) -> bool:
|
||||||
|
"""Check if tier-2 price curve is monotone non-decreasing."""
|
||||||
|
return e <= E_MAX_MONOTONE
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_median(params: "VoteParams") -> "VoteParams":
|
||||||
|
"""
|
||||||
|
Ensure the median curve is physically valid:
|
||||||
|
- Tier 2 must be monotone non-decreasing (penalizes high consumption)
|
||||||
|
- If e violates monotonicity, clamp it to E_MAX_MONOTONE
|
||||||
|
"""
|
||||||
|
e = params.e
|
||||||
|
if not _is_tier2_monotone(e):
|
||||||
|
e = E_MAX_MONOTONE
|
||||||
|
|
||||||
|
return VoteParams(
|
||||||
|
vinf=params.vinf,
|
||||||
|
a=params.a,
|
||||||
|
b=params.b,
|
||||||
|
c=params.c,
|
||||||
|
d=params.d,
|
||||||
|
e=e,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def compute_median(votes: list[VoteParams]) -> VoteParams | None:
|
def compute_median(votes: list[VoteParams]) -> VoteParams | None:
|
||||||
"""
|
"""
|
||||||
Compute element-wise median of vote parameters.
|
Compute element-wise median of vote parameters.
|
||||||
|
|
||||||
Returns None if no votes provided.
|
Returns None if no votes provided.
|
||||||
|
The result is sanitized for tier-2 monotonicity.
|
||||||
"""
|
"""
|
||||||
if not votes:
|
if not votes:
|
||||||
return None
|
return None
|
||||||
@@ -38,7 +74,7 @@ def compute_median(votes: list[VoteParams]) -> VoteParams | None:
|
|||||||
d_s = [v.d for v in votes]
|
d_s = [v.d for v in votes]
|
||||||
e_s = [v.e for v in votes]
|
e_s = [v.e for v in votes]
|
||||||
|
|
||||||
return VoteParams(
|
raw = VoteParams(
|
||||||
vinf=float(np.median(vinfs)),
|
vinf=float(np.median(vinfs)),
|
||||||
a=float(np.median(a_s)),
|
a=float(np.median(a_s)),
|
||||||
b=float(np.median(b_s)),
|
b=float(np.median(b_s)),
|
||||||
@@ -46,3 +82,5 @@ def compute_median(votes: list[VoteParams]) -> VoteParams | None:
|
|||||||
d=float(np.median(d_s)),
|
d=float(np.median(d_s)),
|
||||||
e=float(np.median(e_s)),
|
e=float(np.median(e_s)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return sanitize_median(raw)
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# docker-compose.jetson.yml
|
||||||
|
# Ports décalés (8001/3001) pour ne pas entrer en conflit
|
||||||
|
# avec les services existants sur le Jetson.
|
||||||
|
# Nginx fait le reverse proxy depuis sejeteraleau.nicolasboyer.com
|
||||||
|
|
||||||
|
services:
|
||||||
|
|
||||||
|
backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/backend.Dockerfile
|
||||||
|
target: production
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: sqlite+aiosqlite:////data/sejeteralo.db
|
||||||
|
SECRET_KEY: CHANGEZ-MOI-cle-longue-et-aleatoire-32-chars-min
|
||||||
|
DEBUG: "false"
|
||||||
|
CORS_ORIGINS: '["https://sejeteraleau.nicolasboyer.com"]'
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:8010:8000"
|
||||||
|
volumes:
|
||||||
|
- backend-data:/data
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/frontend.Dockerfile
|
||||||
|
target: production
|
||||||
|
args:
|
||||||
|
# URL vue depuis le navigateur du visiteur
|
||||||
|
NUXT_PUBLIC_API_BASE: https://sejeteraleau.nicolasboyer.com/api/v1
|
||||||
|
environment:
|
||||||
|
NUXT_PUBLIC_API_BASE: https://sejeteraleau.nicolasboyer.com/api/v1
|
||||||
|
PORT: "3000"
|
||||||
|
ports:
|
||||||
|
- "127.0.0.1:3010:3000"
|
||||||
|
depends_on:
|
||||||
|
- backend
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
backend-data:
|
||||||
Reference in New Issue
Block a user