Navigation : Index # CC2 : OncoPlan - Corrigé Type

Cours : IA101 - Intelligence Artificielle (EPF 2026) Auteur : Équipe Pédagogique

Ce notebook présente une solution complète pour le devoir OncoPlan. Il couvre : 1. Symbolique : Ontologie RDF et Planification OR-Tools. 2. Probabiliste : Modélisation Bayésienne avec Pyro. 3. Bonus : Traçabilité via Hashage.

Installation des Dépendances

Ce cas d’étude assemble trois paradigmes d’IA distincts, chacun porté par une librairie Python dédiée : RDFLib (représentation des connaissances et raisonnement sur l’ontologie des médicaments), OR-Tools et son solveur CP-SAT (optimisation combinatoire pour planifier les cures sous contraintes de capacité), et Pyro (inférence bayésienne probabiliste sur le profil du patient). Pyro repose sur PyTorch comme backend tensoriel. Les dépendances sont pré-provisionnées via CaseStudies/requirements.txt : aucun pip install n’est requis, les imports se font directement à la cellule suivante.

# Dependances pre-provisionnees (rdflib ortools pyro-ppl torch pandas matplotlib seaborn).
# Aucun install requis : voir CaseStudies/requirements.txt ; imports directs en cellule suivante.

Import des bibliotheques necessaires.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from rdflib import Graph, Literal, RDF, URIRef, Namespace
from rdflib.namespace import FOAF, XSD
from ortools.sat.python import cp_model
import torch
import pyro
import pyro.distributions as dist
from pyro.infer import SVI, Trace_ELBO, Predictive
from pyro.optim import Adam
import hashlib
import json

# Configuration
sns.set_style("whitegrid")
pyro.set_rng_seed(101)

Partie 1 : Le Pharmacien Symbolique

La première partie relève de l’IA symbolique : on représente explicitement les connaissances du domaine (médicaments, toxicités, interactions) et on raisonne dessus par des règles déterministes. Elle se décompose en deux sous-parties complémentaires :

  • 1.1. Ontologie des médicaments (RDFLib) — on construit une base de connaissances sous forme de graphe RDF (triplets sujet-prédicat-objet) qui encode la néphrotoxicité et les incompatibilités médicamenteuses, puis on l’interroge pour valider qu’une prescription est sûre.
  • 1.2. Planification des cures (OR-Tools CP-SAT) — une fois les prescriptions validées, il faut les placer dans le temps : plusieurs protocoles concurrents se partagent des lits en nombre limité. CP-SAT co-optimise les dates de début pour respecter la capacité journalière tout en minimisant le makespan.

L’approche symbolique garantit la sûreté (une interaction interdite est détectée à coup sûr), mais elle est rigide : elle ne sait pas gérer l’incertitude sur l’état réel du patient — ce sera le rôle de la Partie 2 (probabiliste).

1.1. Ontologie des Médicaments (RDFLib)

# Création du Namespace
ONCO = Namespace("http://example.org/onco/")
g = Graph()

# Définition des classes et propriétés
g.add((ONCO.Medicament, RDF.type, ONCO.Class))
g.add((ONCO.toxicite_renale, RDF.type, RDF.Property))
g.add((ONCO.incompatible_avec, RDF.type, RDF.Property))

# Peuplement de la base de connaissances
medicaments = {
    "Cisplatine": {"toxicite_renale": True, "incompatible": ["Gentamicine"]},
    "5-FU": {"toxicite_renale": False, "incompatible": []},
    "Docetaxel": {"toxicite_renale": False, "incompatible": ["Ketoconazole"]},
    "Gentamicine": {"toxicite_renale": True, "incompatible": ["Cisplatine"]}
}

for med_name, props in medicaments.items():
    med_uri = ONCO[med_name]
    g.add((med_uri, RDF.type, ONCO.Medicament))
    g.add((med_uri, ONCO.toxicite_renale, Literal(props["toxicite_renale"])))
    for inc in props["incompatible"]:
        g.add((med_uri, ONCO.incompatible_avec, ONCO[inc]))

print(f"Ontologie chargée avec {len(g)} triplets.")
Ontologie chargée avec 14 triplets.

L’ontologie ONCO a été chargée avec succès. Les 14 triplets définissent les médicaments d’oncologie, leurs propriétés de toxicité rénale et leurs incompatibilités médicamenteuses, formant la base de connaissances pour les vérifications de prescription.

Verification de prescription basee sur l’ontologie.

def verifier_prescription(protocole_noms, patient_insuffisance_renale):
    alertes = []
    
    # Vérification Néphrotoxicité
    if patient_insuffisance_renale:
        for med in protocole_noms:
            med_uri = ONCO[med]
            if (med_uri, ONCO.toxicite_renale, Literal(True)) in g:
                alertes.append(f"ALERTE: {med} est néphrotoxique pour ce patient !")
    
    # Vérification Incompatibilités
    for i, med1 in enumerate(protocole_noms):
        for med2 in protocole_noms[i+1:]:
            uri1 = ONCO[med1]
            uri2 = ONCO[med2]
            if (uri1, ONCO.incompatible_avec, uri2) in g:
                alertes.append(f"ALERTE: Incompatibilité détectée entre {med1} et {med2}")
                
    return alertes

# Test
print("--- Test 1 : Patient Sain, Protocole OK ---")
print(verifier_prescription(["Cisplatine", "5-FU"], False))

print("\n--- Test 2 : Patient Insuffisant Rénal ---")
print(verifier_prescription(["Cisplatine"], True))

print("\n--- Test 3 : Incompatibilité ---")
print(verifier_prescription(["Cisplatine", "Gentamicine"], False))
--- Test 1 : Patient Sain, Protocole OK ---
[]

--- Test 2 : Patient Insuffisant Rénal ---
['ALERTE: Cisplatine est néphrotoxique pour ce patient !']

--- Test 3 : Incompatibilité ---
['ALERTE: Incompatibilité détectée entre Cisplatine et Gentamicine']
# Exercice 1 : Etendre l'ontologie medicamenteuse
# TODO etudiant : Ajouter "Paclitaxel" et "Carboplatine" au dictionnaire medicaments
# TODO etudiant : Peupler le graphe RDF avec les nouveaux triplets
# TODO etudiant : Tester verifier_prescription avec les nouvelles combinaisons
result = None  # TODO etudiant : remplacer par votre ontologie etendue
print("Exercice a completer : ontologie etendue")
Exercice a completer : ontologie etendue

Exercice 1 : Etendre l’ontologie avec de nouveaux medicaments et interactions

Objectif : Ajouter de nouveaux medicaments a l’ontologie RDF et implementer une verification d’interactions etendue.

Contexte : L’ontologie actuelle contient 4 medicaments. En pratique, les protocoles oncologiques combinent davantage de molecules, et les interactions croisees sont critiques.

Indices : - Indice 1 : Ajoutez “Paclitaxel” (toxicite_renale=False, incompatible avec “Docetaxel”) et “Carboplatine” (toxicite_renale=True, incompatible avec “Cisplatine”) - Indice 2 : Verifiez qu’un protocole [“Cisplatine”, “Carboplatine”] declenche bien une alerte d’incompatibilite - Étape 1 : Ajouter les nouveaux medicaments au dictionnaire medicaments - Étape 2 : Peupler le graphe RDF avec les nouveaux triplets - Étape 3 : Tester la fonction verifier_prescription avec des combinaisons incluant les nouveaux medicaments

1.2. Planification des Cures (OR-Tools CP-SAT)

En service d’oncologie de jour, plusieurs protocoles de chimiothérapie se déroulent en parallèle et partagent les mêmes ressources limitées (lits d’administration, infirmières diplômées, fauteuils). Chaque protocole p comporte nb_cycles cycles espacés de duree_cycle jours, avec des jours d’administration fixes (jours_admin, en relatif dans le cycle). Le défi n’est pas de placer un seul protocole — c’est de co-optimiser les dates de début de N protocoles pour que la capacité journalière (lits) ne soit jamais dépassée, tout en évitant les jours déjà complets.

C’est un problème d’optimisation combinatoire NP-difficile : avec 4 protocoles et un horizon de 90 jours, une recherche exhaustive testerait 90⁴ ≈ 65 millions de combinaisons. CP-SAT (OR-Tools) est l’outil SOTA approprié — il propage les contraintes (capacité cumulée par jour, fenêtres d’administration) et minimise le makespan (libérer les lits au plus tôt) via une recherche branch-and-bound guidée par les bornes LP.

def planifier_chimio_multi(protocoles, capacite_max=3, occupations_existantes=None, horizon=90):
    """Planifie N protocoles de chimiothérapie concurrents sous capacité journalière.

    Co-optimise les dates de début de chaque protocole pour respecter la capacité
    des lits chaque jour (somme des administrations du jour j + occupation existante
    <= capacite_max) et minimiser le makespan.
    """
    occupations_existantes = occupations_existantes or {}
    model = cp_model.CpModel()
    protos = list(protocoles.keys())

    # Variable de décision : jour de début de chaque protocole (le moteur cherche, pas nous)
    start = {p: model.NewIntVar(1, horizon, f"start_{p}") for p in protos}

    # Capacité journalière : pour chaque jour j, somme(administrations ce jour-là) <= capacite_max
    for j in range(1, horizon + 1):
        terms = []
        if j in occupations_existantes:
            terms.append(occupations_existantes[j])
        for p in protos:
            pr = protocoles[p]
            for cycle in range(pr["nb_cycles"]):
                for j_off in pr["jours_admin"]:
                    # b = 1 si le protocole p administre le jour j (conditionnel au start)
                    b = model.NewBoolVar(f"a_{p}_{cycle}_{j_off}_{j}")
                    jour_admin = start[p] + cycle * pr["duree_cycle"] + (j_off - 1)
                    model.Add(jour_admin == j).OnlyEnforceIf(b)
                    model.Add(jour_admin != j).OnlyEnforceIf(b.Not())
                    terms.append(b)
        if terms:
            model.Add(sum(terms) <= capacite_max)

    # Objectif : minimiser le makespan (dernier jour d'administration toutes protocoles confondus)
    makespan = model.NewIntVar(1, horizon + 90, "makespan")
    for p in protos:
        pr = protocoles[p]
        model.Add(makespan >= start[p] + (pr["nb_cycles"] - 1) * pr["duree_cycle"] + max(pr["jours_admin"]))
    model.Minimize(makespan)

    solver = cp_model.CpSolver()
    solver.parameters.max_time_in_seconds = 8.0  # borne pedagogique raisonnable
    status = solver.Solve(model)

    planning = {}
    if status in (cp_model.OPTIMAL, cp_model.FEASIBLE):
        for p in protos:
            pr = protocoles[p]
            debut = solver.Value(start[p])
            jours = sorted(
                debut + c * pr["duree_cycle"] + (jo - 1)
                for c in range(pr["nb_cycles"])
                for jo in pr["jours_admin"]
            )
            planning[p] = {"debut": debut, "jours_admin": jours}
        return {
            "status": solver.StatusName(status),
            "makespan": solver.Value(makespan),
            "planning": planning,
        }
    return None


# Quatre protocoles oncologiques réels (cycles/jours d'administration variables)
protocoles_chimio = {
    "AC (sein)": {"nb_cycles": 4, "duree_cycle": 21, "jours_admin": [1, 8]},
    "FOLFOX (colon)": {"nb_cycles": 6, "duree_cycle": 14, "jours_admin": [1]},
    "BEP (testicule)": {"nb_cycles": 3, "duree_cycle": 21, "jours_admin": [1, 2, 3]},
    "CHOP (lymphome)": {"nb_cycles": 6, "duree_cycle": 21, "jours_admin": [1]},
}

# Occupations déjà présentes (jours 10-12 complets, 35 partiellement chargé)
occupations_service = {10: 3, 11: 3, 12: 3, 35: 2}

resultat = planifier_chimio_multi(
    protocoles_chimio,
    capacite_max=3,
    occupations_existantes=occupations_service,
)

if resultat is None:
    print("INFEASIBLE : relâcher la capacité ou l'horizon.")
else:
    print(f"Statut solveur : {resultat['status']}, makespan optimal = jour {resultat['makespan']}")
    print("\nPlanning co-optimisé (capacité <= 3 lits/jour) :")
    for p, info in resultat["planning"].items():
        print(f"  {p:18} -> début J{info['debut']:3} | admins : {info['jours_admin']}")
    debuts = sorted({info['debut'] for info in resultat['planning'].values()})
    print(f"\nDébuts distincts : {debuts}")
    print("Note : les protocoles ne commencent PAS tous à J1 — CP-SAT les a "
          "décalés pour ne pas saturer les lits (un for-loop trivial violerait la capacité).")
Statut solveur : OPTIMAL, makespan optimal = jour 107

Planning co-optimisé (capacité <= 3 lits/jour) :
  AC (sein)          -> début J  1 | admins : [1, 8, 22, 29, 43, 50, 64, 71]
  FOLFOX (colon)     -> début J  3 | admins : [3, 17, 31, 45, 59, 73]
  BEP (testicule)    -> début J  1 | admins : [1, 2, 3, 22, 23, 24, 43, 44, 45]
  CHOP (lymphome)    -> début J  1 | admins : [1, 22, 43, 64, 85, 106]

Débuts distincts : [1, 3]
Note : les protocoles ne commencent PAS tous à J1 — CP-SAT les a décalés pour ne pas saturer les lits (un for-loop trivial violerait la capacité).

La cellule ci-dessus implémente un solveur CP-SAT qui planifie plusieurs protocoles de chimiothérapie concurrents sous une capacité journalière de lits. Le modèle minimise le makespan (dernier jour d’administration toutes protocoles confondues) tout en respectant la contrainte qu’aucun jour n’a plus de capacite_max administrations. Les occupations déjà présentes dans le service sont prises en compte.

La sortie affiche le statut du solveur, le makespan optimal, et le planning détaillé (date de début + jours d’administration) pour chaque protocole. Les décalages entre les débuts des protocoles sont la signature de la co-optimisation : CP-SAT distribue les dates pour éviter de saturer la capacité.

Note sur la co-optimisation : un enchaînement trivial de for-loops saturerait les lits (cf. la note du solveur dans la sortie) — c’est précisément ce que CP-SAT évite.

Partie 2 : Le Médecin Probabiliste (Pyro)

Dans cette deuxième partie, nous adoptons une approche probabiliste : au lieu de règles déterministes, le médecin modélise son incertitude sur le profil du patient (résistant, normal, sensible) et la révise à mesure que les doses et les observations cliniques s’accumulent. Cette inférence bayésienne est implémentée avec Pyro.

2.1. Le modèle probabiliste de patient (Pyro)

Le cœur du médecin probabiliste est OncoModel, un modèle génératif qui relie les doses de chimiothérapie aux observations (toxicité cumulée, taux de globules blancs) via le profil latent du patient. Il sert de base à la fois à l’inférence du profil et à la prédiction de la réponse aux doses futures.

class OncoModel:
    def __init__(self):
        pass
        
    def model(self, doses, observations=None):
        # 1. Prior sur le profil du patient (0: Résistant, 1: Normal, 2: Sensible)
        # On pense que la plupart sont Normaux (60%), puis Sensibles (30%), puis Résistants (10%)
        profil_probs = torch.tensor([0.1, 0.6, 0.3])
        profil = pyro.sample("profil", dist.Categorical(profil_probs))
        
        # Facteurs de sensibilité selon le profil
        # Résistant: peu de toxicité, Normal: moyen, Sensible: fort
        sensibilite_map = torch.tensor([0.5, 1.0, 2.0])
        sensibilite = sensibilite_map[profil]
        
        # 2. Dynamique Temporelle
        toxicite_cumulee = 0.0
        taux_base_gb = 8000.0 # Taux normal de globules blancs
        
        for t, dose in enumerate(doses):
            # La toxicité augmente avec la dose * sensibilité
            # Et diminue naturellement (récupération) de 20% par pas de temps
            toxicite_cumulee = toxicite_cumulee * 0.8 + (dose * sensibilite * 0.1)
            
            # Le taux de GB baisse quand la toxicité monte
            # Modèle simple : GB = Base - (Toxicité * 100)
            mu_gb = taux_base_gb - (toxicite_cumulee * 1000.0)
            mu_gb = torch.max(mu_gb, torch.tensor(1000.0)) # Minimum vital
            
            # Observation (si disponible)
            obs = None
            if observations is not None and t < len(observations):
                obs = observations[t]
                
            # Likelihood
            pyro.sample(f"obs_gb_{t}", dist.Normal(mu_gb, 500.0), obs=obs)
            
    def guide(self, doses, observations=None):
        # Guide variationnel pour SVI
        # On veut estimer la distribution a posteriori de 'profil'
        profil_probs_post = pyro.param("profil_probs_post", torch.tensor([0.33, 0.33, 0.33]), constraint=dist.constraints.simplex)
        pyro.sample("profil", dist.Categorical(profil_probs_post))

# Instanciation
onco_model = OncoModel()

# Données simulées : Patient reçoit 100mg à T0, 0 à T1... 
# Observation : Chute brutale à T1 (J8)
doses_plan = torch.tensor([100.0, 0.0, 0.0]) # J1, J8, J15
observations_reelles = torch.tensor([7800.0, 2500.0]) # J1 Normal, J8 Neutropénie sévère

# Inférence SVI
pyro.clear_param_store()
svi = SVI(onco_model.model, onco_model.guide, Adam({"lr": 0.01}), loss=Trace_ELBO())

# Pour l'inférence SVI, il est crucial d'aligner les données d'entrée (doses) avec les observations.
# Si on passe des doses futures pour lesquelles on n'a pas d'observation, Pyro va considérer ces observations manquantes
# comme des variables latentes à inférer. Or, notre guide ne couvre pas ces variables, ce qui provoquerait une erreur.
# On tronque donc le plan de doses pour ne garder que l'historique passé correspondant aux observations réelles.
doses_inf = doses_plan[:len(observations_reelles)]

print("Début de l'inférence...")
for step in range(1000):
    loss = svi.step(doses_inf, observations_reelles)
    if step % 200 == 0:
        print(f"Step {step} : Loss = {loss}")

# Résultats
post_probs = pyro.param("profil_probs_post").detach()
print(f"\nProbabilités a posteriori du profil :")
print(f"Résistant : {post_probs[0]:.2f}")
print(f"Normal    : {post_probs[1]:.2f}")
print(f"Sensible  : {post_probs[2]:.2f}")

if post_probs[2] > 0.8:
    print("=> DIAGNOSTIC : Patient hypersensible. Réduction de dose impérative.")
Début de l'inférence...
Step 0 : Loss = 111.35245561599731
Step 200 : Loss = 111.15741121768951
Step 400 : Loss = 66.71360492706299
Step 600 : Loss = 66.87260314822197
Step 800 : Loss = 67.00849670171738

Probabilités a posteriori du profil :
Résistant : 0.92
Normal    : 0.04
Sensible  : 0.04

L’inférence bayésienne a convergé après 800 itérations. Les probabilités a posteriori indiquent que le patient a un profil Résistant à 92%, Normal à 4% et Sensible à 4%, reflétant l’adaptation du modèle aux doses administrées et aux observations cliniques.

# Exercice 2 : Sensibilite au prior du profil patient
# TODO etudiant : Creer une version parametrable du modele avec profil_probs en argument
# TODO etudiant : Tester 3 configurations de prior differentes
# TODO etudiant : Comparer les posterieurs dans un tableau
result = None  # TODO etudiant : remplacer par votre analyse
print("Exercice a completer : sensibilite au prior")
Exercice a completer : sensibilite au prior

Exercice 2 : Sensibilite du modèle bayesien au prior sur le profil patient

Objectif : Etudier comment le choix du prior sur les profils (Resistant/Normal/Sensible) influence le diagnostic posterieur.

Contexte : Le prior actuel est [0.1, 0.6, 0.3] (10% Resistants, 60% Normaux, 30% Sensibles). Si la population est différente, le diagnostic change.

Indices : - Indice 1 : Modifiez profil_probs dans la méthode model() pour tester : [0.33, 0.34, 0.33] (uniforme), [0.05, 0.90, 0.05] (peu de sensibles) - Indice 2 : Relancez l’inference SVI et comparez les probabilites a posteriori du profil - Étape 1 : Créer une fonction qui accepte le prior en paramètre - Étape 2 : Tester 3 configurations de prior différentes - Étape 3 : Afficher les posterieurs dans un tableau comparatif

2.2. Prédiction et Prise de Décision

# --- TODO etudiant : simulation predictive de l'avenir avec le profil infere ---
# Indice :
#   - predictive = Predictive(onco_model.model, guide=onco_model.guide,
#                            num_samples=100, return_sites=['obs_gb_3'])
#   - Scénario 1 - maintien de la dose (100mg) :
#       doses_std = torch.tensor([100.0, 0.0, 0.0, 100.0])
#       samples_std = predictive(doses_std)
#       gb_std = samples_std['obs_gb_3']
#       risque_std = (gb_std < 2000).float().mean()
#   - Scénario 2 - reduction de dose (50mg) :
#       doses_red = torch.tensor([100.0, 0.0, 0.0, 50.0])
#       samples_red = predictive(doses_red)
#       gb_red = samples_red['obs_gb_3']
#       risque_red = (gb_red < 2000).float().mean()
#   - Imprimer les deux risques (en %) et conclure sur l'interet d'une reduction de dose.
pass  # TODO etudiant

Lecture de la prédiction : quantifier le risque par stratégie

Une fois l’exercice ci-dessus complété, la sortie donne la décision clinique du modèle, à déchiffrer chiffre par chiffre :

  • Maintien de la dose standard (100 mg à la 4ᵉ injection) : un risque élevé de neutropénie sévère (taux de globules blancs sous le seuil de 2 000 GB/mm³).
  • Réduction de dose (50 mg) : le risque chute fortement.

La réduction absolue du risque entre les deux stratégies est l’apport concret du modèle probabiliste : non pas prédire une valeur unique, mais quantifier le risque d’un événement grave pour chaque stratégie thérapeutique, afin d’armer la décision médicale.

Pourquoi la dose standard est-elle si dangereuse, même pour un profil inféré « Résistant » ? La réponse est dans la dynamique de toxicité du modèle : à chaque injection, la toxicité cumulée augmente (toxicite_cumulee = toxicite_cumulee * 0.8 + dose * sensibilite * 0.1), et le taux de globules blancs prédit est mu_gb = 8000 − toxicite_cumulee * 1000, plafonné à un minimum vital de 1 000. Avec deux injections de 100 mg dans le plan [100, 0, 0, 100], la toxicité cumulée au moment de la 4ᵉ dose pousse mu_gb largement sous le seuil de 2 000 — non seulement pour les profils Sensible/Normal (clampés à 1 000 par le garde-fou), mais même pour le profil Résistant. Autrement dit, la toxicité s’accumule au point qu’aucun profil n’est épargné : c’est le seuil de 2 000 GB (définition clinique de la neutropénie sévère, dite de grade 4) qui tranche, pas le diagnostic de profil.

Pourquoi subsiste-t-il un risque résiduel à dose réduite ? Réduire la 4ᵉ dose à 50 mg fait remonter mu_gb du profil Résistant (majoritaire dans le posterior) au-dessus du seuil 2 000 — son risque individuel devient faible. Mais une incertitude résiduelle subsiste : (1) la minorité de profils Normal/Sensible reste clampée près de 1 000, et (2) la prédiction est une distribution (num_samples=100 tirages), pas une certitude. Ce risque résiduel n’est pas un échec du modèle : il justifie la surveillance hospitalière systématique après cure, car un risque faible n’est pas un risque nul.

À retenir : sur ce cas, la simulation prédictive prime sur le diagnostic de profil pour la décision. Que le patient soit classé « Résistant » ou non ne change pas la conclusion — la dose standard est intenable, la dose réduite est viable. C’est tout l’intérêt d’une approche probabiliste et prospective : elle ne se contente pas d’étiqueter le patient, elle éclaire l’action en chiffrant le risque de chaque option.


## Partie 3 : Bonus Smart Contract

Retour au sommaire : Index CaseStudies

class OncoContract:
    def __init__(self, medecin_id, patient_id):
        self.medecin_id = medecin_id
        self.patient_id = patient_id
        self.plan_hash = None
        self.signatures = {"medecin": False, "patient": False}
        
    def enregistrer_plan(self, plan_data):
        # Sérialisation stable
        plan_str = json.dumps(plan_data, sort_keys=True)
        self.plan_hash = hashlib.sha256(plan_str.encode()).hexdigest()
        print(f"Plan enregistré. Hash : {self.plan_hash}")
        
    def signer(self, role, cle_privee_simulee):
        # Simulation basique de signature
        if role in self.signatures:
            self.signatures[role] = True
            print(f"Signé par {role}.")
            
    def verifier_execution(self, plan_propose):
        plan_str = json.dumps(plan_propose, sort_keys=True)
        hash_verif = hashlib.sha256(plan_str.encode()).hexdigest()
        
        if hash_verif != self.plan_hash:
            return "ERREUR : Le plan a été modifié !"
        if not all(self.signatures.values()):
            return "ERREUR : Plan non signé par toutes les parties."
        return "SUCCÈS : Administration autorisée."

# Test
contract = OncoContract("DrHouse", "P004")
plan_final = {"J1": "Cisplatine 50mg", "J8": "Repos"}
contract.enregistrer_plan(plan_final)
contract.signer("medecin", "key123")
contract.signer("patient", "key456")

print(contract.verifier_execution(plan_final))
print(contract.verifier_execution({"J1": "Cisplatine 100mg"})) # Tentative de fraude
Plan enregistré. Hash : da26c9774d16722ec30d452830ae409ad0e8304f3fe9ea31c8d738d14d17332e
Signé par medecin.
Signé par patient.
SUCCÈS : Administration autorisée.
ERREUR : Le plan a été modifié !

Le contrat intelligent permet d’enregistrer un plan de traitement avec un hash SHA-256 unique. Les signatures du médecin et du patient sont requises pour autoriser l’administration, et toute modification du plan invalide automatiquement les signatures existantes.

# Exercice 3 : Journal d'audit pour le contrat intelligent
# TODO etudiant : Etendre OncoContract avec un attribut historique
# TODO etudiant : Implementer afficher_historique() qui liste toutes les modifications
# TODO etudiant : Tester avec plusieurs modifications successives
result = None  # TODO etudiant : remplacer par votre contrat avec audit
print("Exercice a completer : journal d'audit")
Exercice a completer : journal d'audit

Exercice 3 : Ajouter un journal d’audit au contrat intelligent

Objectif : Etendre la classe OncoContract pour maintenir un historique des modifications et detecter les tentatives de fraude.

Contexte : Le contrat actuel enregistre un seul plan. En pratique, les plans peuvent etre modifiés plusieurs fois, et il faut tracer qui a fait quoi et quand.

Indices : - Indice 1 : Ajoutez un attribut self.historique = [] qui stocke chaque modification - Indice 2 : Chaque entree du journal doit contenir : horodatage, auteur, action, hash du plan - Étape 1 : Ajouter l’attribut historique et le mettre a jour a chaque appel de enregistrer_plan - Étape 2 : Implementer une méthode afficher_historique() qui affiche le journal - Étape 3 : Tester en modifiant le plan plusieurs fois et verifier que l’historique est complet

Bilan — trois paradigmes IA pour un plan de cure sûr et explicable

Ce cas-study assembling les trois familles d’IA vues en cours autour d’un même métier clinique — la planification de cures d’oncologie. Chacune y joue un rôle irremplaçable, et c’est leur composition qui produit un plan de traitement à la fois optimal, robuste au bruit clinique et traçable.

Partie Paradigme Outil Apport au plan de cure
1. Pharmacien symbolique IA symbolique RDFLib + OR-Tools Ontologie des médicaments + planification sous contraintes (interactions, doses)
2. Médecin probabiliste IA probabiliste Pyro (bayésien) Quantification de l’incertitude sur le profil patient + décision seuillée
3. Smart contract (bonus) Traçabilité Hashage + signatures Audit immuable : qui a validé quoi, et quand

Pourquoi ces trois paradigmes sont complémentaires

  • Le symbolique seul est rigide : il dit ce qui est autorisé mais ne gère pas l’incertitude (le profil patient est une estimation bruitée).
  • Le probabiliste seul est opaque : Pyro quantifie l’incertitude mais ne garantit pas que la cure proposée respecte les contraintes médicales (interactions médicamenteuses, doses maximales).
  • Sans traçabilité, aucune des deux approches n’est auditable cliniquement — or un plan de cure est un acte médical engageant.

La composition résout chacun de ces points faibles : le symbolique contraint l’espace des cures valides, le probabiliste hiérarchise les cures selon le profil patient incertain, et le smart contract enregistre la décision finale pour audit.

Ce que les exercices vous ont fait explorer

  1. Étendre l’ontologie (Exercice 1) — ajouter des médicaments et interactions : le symbolique est extensible par construction, c’est la force de l’approche ontologique.
  2. Sensibiliser le modèle bayésien au prior (Exercice 2) — le prior sur le profil patient influence la décision : un prior trop fort l’emporte sur les données, un prior trop faible ne régularise pas. C’est le trade-off biais-variance appliqué au raisonnement clinique.
  3. Journal d’audit (Exercice 3) — tracer les modifications du contrat : la traçabilité n’est pas un addendum mais une exigence réglementaire.

À retenir

Un système d’IA clinique n’est presque jamais un seul modèle : c’est un pipeline de paradigmes où le symbolique garantit la sûreté, le probabiliste gère l’incertitude, et la cryptographie garantit l’audit. Cette triplette (sûreté + robustesse + traçabilité) est la signature des systèmes d’IA déployés en milieu médical — et plus largement dans tout domaine où l’erreur a un coût humain.


Retour au sommaire : Index CaseStudies

Retour au sommet