forked from EHV/sejeteralo
Compare commits
6 Commits
65c148142c
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| db964d44b2 | |||
| b5d58911fb | |||
| 8b1dd9b170 | |||
| daad6bb1d5 | |||
| 129c9cfc7b | |||
| b24f226b35 |
+1
-1
@@ -67,7 +67,7 @@ steps:
|
||||
|
||||
# NOTE: volumes + pas de from_secret : compatible
|
||||
- name: sbom-scan
|
||||
image: aquasec/trivy:latest
|
||||
image: aquasec/trivy:0.70.0
|
||||
volumes:
|
||||
- /home/syoul/trivy-cache:/root/.cache/trivy
|
||||
commands:
|
||||
|
||||
@@ -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:
|
||||
- It's transparent and politically explainable
|
||||
- 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
|
||||
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
|
||||
class VoteParams:
|
||||
"""The 6 citizen-adjustable parameters."""
|
||||
@@ -22,23 +32,49 @@ class VoteParams:
|
||||
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:
|
||||
"""
|
||||
Compute element-wise median of vote parameters.
|
||||
|
||||
Returns None if no votes provided.
|
||||
The result is sanitized for tier-2 monotonicity.
|
||||
"""
|
||||
if not votes:
|
||||
return None
|
||||
|
||||
vinfs = [v.vinf for v in votes]
|
||||
a_s = [v.a for v in votes]
|
||||
b_s = [v.b for v in votes]
|
||||
c_s = [v.c for v in votes]
|
||||
d_s = [v.d for v in votes]
|
||||
e_s = [v.e for v in votes]
|
||||
a_s = [v.a for v in votes]
|
||||
b_s = [v.b for v in votes]
|
||||
c_s = [v.c for v in votes]
|
||||
d_s = [v.d for v in votes]
|
||||
e_s = [v.e for v in votes]
|
||||
|
||||
return VoteParams(
|
||||
raw = VoteParams(
|
||||
vinf=float(np.median(vinfs)),
|
||||
a=float(np.median(a_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)),
|
||||
e=float(np.median(e_s)),
|
||||
)
|
||||
|
||||
return sanitize_median(raw)
|
||||
+6
-3
@@ -17,9 +17,12 @@ from app.services.auth_service import hash_password
|
||||
from app.services.import_service import generate_auth_code
|
||||
from app.engine.pricing import HouseholdData, compute_p0
|
||||
|
||||
XLS_PATH = os.path.join(os.path.dirname(__file__), "Eau2018.xls")
|
||||
if not os.path.exists(XLS_PATH):
|
||||
XLS_PATH = os.path.join(os.path.dirname(__file__), "..", "Eau2018.xls")
|
||||
_XLS_CANDIDATES = [
|
||||
"/opt/Eau2018.xls", # image Docker (hors volume monté sur /app)
|
||||
os.path.join(os.path.dirname(__file__), "Eau2018.xls"),
|
||||
os.path.join(os.path.dirname(__file__), "..", "Eau2018.xls"), # dev local depuis backend/
|
||||
]
|
||||
XLS_PATH = next((p for p in _XLS_CANDIDATES if os.path.exists(p)), _XLS_CANDIDATES[-1])
|
||||
|
||||
|
||||
# Codes fixes — identiques dans le dev hint frontend
|
||||
|
||||
@@ -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:
|
||||
@@ -11,7 +11,7 @@ COPY backend/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY backend/ .
|
||||
COPY Eau2018.xls /app/Eau2018.xls
|
||||
COPY Eau2018.xls /opt/Eau2018.xls
|
||||
|
||||
# Production
|
||||
FROM base AS production
|
||||
@@ -22,6 +22,7 @@ COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3
|
||||
COPY --from=build /usr/local/bin/uvicorn /usr/local/bin/uvicorn
|
||||
COPY --from=build /usr/local/bin/alembic /usr/local/bin/alembic
|
||||
COPY --from=build /app /app
|
||||
COPY --from=build /opt/Eau2018.xls /opt/Eau2018.xls
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/docs')" || exit 1
|
||||
|
||||
@@ -231,7 +231,7 @@
|
||||
</text>
|
||||
|
||||
<!-- Legend box (top-right) -->
|
||||
<g :transform="`translate(${margin.left + plotW - 232}, ${margin.top + 8})`">
|
||||
<g :transform="`translate(${margin.left + 8}, ${margin.top + plotH - 90})`">
|
||||
<rect x="0" y="0" width="220" :height="citizenAbo > 0 ? 80 : 62" rx="6" :fill="t.legendBg" fill-opacity="0.92" :stroke="t.legendBorder" />
|
||||
<line x1="10" y1="14" x2="28" y2="14" stroke="#2563eb" stroke-width="3" stroke-linecap="round" />
|
||||
<text x="34" y="18" font-size="11" :fill="t.text">Consommations foyers</text>
|
||||
@@ -1295,14 +1295,16 @@ const gridPrices2 = computed(() => {
|
||||
return arr
|
||||
})
|
||||
|
||||
function toPolyline(vols: number[] | undefined, vals: number[] | undefined, cyFn: (v: number) => number): string {
|
||||
function toPolyline(vols: number[] | undefined, vals: number[] | undefined, cyFn: (v: number) => number, minVol = 15): string {
|
||||
if (!vols?.length || !vals?.length) return ''
|
||||
const pts: string[] = []
|
||||
for (let i = 0; i < vols.length; i += 4) {
|
||||
pts.push(`${cx2(vols[i]!)},${cyFn(vals[i]!)}`)
|
||||
const v = vols[i]!
|
||||
if (v < minVol) continue
|
||||
pts.push(`${cx2(v)},${cyFn(vals[i]!)}`)
|
||||
}
|
||||
const last = vols.length - 1
|
||||
if (last % 4 !== 0) pts.push(`${cx2(vols[last]!)},${cyFn(vals[last]!)}`)
|
||||
if (last % 4 !== 0 && vols[last]! >= minVol) pts.push(`${cx2(vols[last]!)},${cyFn(vals[last]!)}`)
|
||||
return pts.join(' ')
|
||||
}
|
||||
|
||||
@@ -1391,7 +1393,7 @@ const margPriceLine = computed(() => {
|
||||
const pts: string[] = []
|
||||
for (let i = 0; i < vols.length; i += 3) {
|
||||
const v = vols[i]!
|
||||
if (v > margVolMax.value) break
|
||||
if (v < 15 || v > margVolMax.value) continue // ← changer break par continue, et ajouter v < 15
|
||||
pts.push(`${margCx(v)},${margCyPrice(prices[i]!)}`)
|
||||
}
|
||||
return pts.join(' ')
|
||||
@@ -1406,7 +1408,7 @@ const margAvgPriceWithAboLine = computed(() => {
|
||||
const pts: string[] = []
|
||||
for (let i = 0; i < vols.length; i += 3) {
|
||||
const v = vols[i]!
|
||||
if (v < 1 || v > margVolMax.value) continue
|
||||
if (v < 15 || v > margVolMax.value) continue
|
||||
const avgP = citizenAbo.value / v + p0lin
|
||||
if (avgP <= margPriceMax.value * 1.5) {
|
||||
pts.push(`${margCx(v)},${margCyPrice(avgP)}`)
|
||||
|
||||
Reference in New Issue
Block a user