# planarian/models.py from django.db import models #from django.conf import settings from django.utils.translation import gettext_lazy as _ from django.contrib.auth.models import User from scanner.models import Experiment, Well WELL_CHOICES = [] def get_well_choices(): wells = Well.objects.order_by('name').all() for well in wells: WELL_CHOICES.append((well.name, well.name)) class ExperimentConfig(models.Model): """ Paramètres d'une expérience PlanarianScanner. Peut être créé depuis Django admin, une vue formulaire ou un import CSV. """ author = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Auteur", null=True, blank=True) experiment_key = models.ForeignKey(Experiment, verbose_name="Expérience", on_delete=models.CASCADE, null=True, blank=False) experiment = models.CharField(_("Identifiant expérience"), max_length=128, null=True, blank=False, default='Identifier' ) well = models.CharField(_("Puit"), help_text=_("Nom du puit"), max_length=8, choices=WELL_CHOICES, null=True, blank=False, default='A1' ) description = models.TextField( blank=True, verbose_name=_("Description"), default="-") created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("Créé le")) active = models.BooleanField(_("Active"), default=True) # --- Calibration optique --- # px_per_mm, fps, well_radius_mm px_per_mm = models.FloatField( default=26.25, verbose_name=_("Pixels par mm"), help_text=_("Facteur de calibration optique"), ) fps = models.FloatField( default=5.0, verbose_name=_("FPS de capture"), help_text=_("Image de capture en img/s"), ) well_radius_mm = models.FloatField( default=8.0, verbose_name=_("Rayon du puits"), help_text=_("En mm"), ) # --- Seuils de mobilité EthoVision --- thresh_immobile = models.FloatField( default=0.2, verbose_name=_("Seuil Immobile (mm/s)"), ) thresh_mobile = models.FloatField( default=1.5, verbose_name=_("Seuil Mobile (mm/s)"), ) # --- Tracker --- tube_axis = models.CharField( max_length=10, default="vertical", choices=[("vertical", _("Vertical")), ("horizontal", _("Horizontal"))], verbose_name=_("Axe du tube"), ) min_area_px = models.IntegerField( default=20, verbose_name=_("Surface min détection (px²)"), ) max_area_ratio = models.FloatField( default=0.10, verbose_name=_("Surface max contour (fraction de la frame)"), help_text=_("Ratio de la surface du puits, ex: 0.10 pour 10%"), ) planarian_count = models.IntegerField( default=1, verbose_name=_("Nombre de planaires"), ) merge_kernel_size = models.PositiveIntegerField( _("Taille du kernel"), help_text=_("taille du kernel elliptique de fusion des fragments (px). Augmenter si fragments résiduels"), default=15 ) min_contour_dist_px = models.PositiveIntegerField( _("Distance "), help_text=_("Distance min entre deux contours pour les considérer comme individus distincts. Défaut : 40px. Augmenter si IDs multiples persistent"), default=40 ) # --- Thigmotactisme --- thigmotaxis_wall_dist_mm = models.FloatField( default=1.0, verbose_name=_("Distance paroi thigmotactisme (mm)"), ) # --- Phototactisme --- PHOTO_MODES = [ ("none", _("Désactivé")), ("fixed", _("Source fixe")), ("sine", _("Source sinusoïdale")), ("radial", _("Gradient radial")), ] photo_mode = models.CharField( max_length=10, default="none", choices=PHOTO_MODES, verbose_name=_("Mode phototactisme"), ) photo_strength = models.FloatField(default=0.0, verbose_name=_("Intensité phototactisme")) photo_x = models.FloatField(default=0.5, verbose_name=_("Source lumière X (0-1)")) photo_y = models.FloatField(default=0.5, verbose_name=_("Source lumière Y (0-1)")) # --- Chimiotactisme --- chemo_strength = models.FloatField(default=0.0, verbose_name=_("Intensité chimiotactisme")) chemo_x = models.FloatField(default=0.5, verbose_name=_("Nourriture X (0-1)")) chemo_y = models.FloatField(default=0.5, verbose_name=_("Nourriture Y (0-1)")) chemo_radius_mm = models.FloatField(default=2.0, verbose_name=_("Rayon nourriture (mm)")) # --- Interactions inter-individus --- avoid_radius_mm = models.FloatField(default=3.0, verbose_name=_("Rayon évitement (mm)")) aggreg_radius_mm = models.FloatField(default=6.0, verbose_name=_("Rayon agrégation (mm)")) class Meta: verbose_name = _("Configuration d'une expérience") verbose_name_plural = _("Configurations des expériences") unique_together = ("experiment_key", "well") ordering = ["-created_at"] def __str__(self): return f"{self.experiment_key}:{self.well}" def save(self, *args, **kwargs): if not self.author: self.author = self.experiment_key.author self.experiment = self.experiment_key.identifier super().save(*args, **kwargs) def get_session(self): return self.experiment_key.session_experiments.first() if self.experiment_key else None def to_params_dict(self) -> dict: """Retourne un dict compatible avec ExperimentParams.""" return { "experiment": self.experiment, "well": self.well, "px_per_mm": self.px_per_mm, "fps": self.fps, "well_radius_mm": self.well_radius_mm, "thresh_immobile": self.thresh_immobile, "thresh_mobile": self.thresh_mobile, "tube_axis": self.tube_axis, "min_area_px": self.min_area_px, "max_area_ratio": self.max_area_ratio, "merge_kernel_size": self.merge_kernel_size, "min_contour_dist_px": self.min_contour_dist_px, "planarian_count": self.planarian_count, "thigmotaxis_wall_dist_mm": self.thigmotaxis_wall_dist_mm, "photo_mode": self.photo_mode, "photo_strength": self.photo_strength, "chemo_strength": self.chemo_strength, "chemo_x": self.chemo_x, "chemo_y": self.chemo_y, "chemo_radius_mm": self.chemo_radius_mm, "avoid_radius_mm": self.avoid_radius_mm, "aggreg_radius_mm": self.aggreg_radius_mm, }