Menu

Module 0 · Section 0.1

Introduction et reproductibilité

55 min · niveau debutant · dernière révision : 2026-11-01

Objectifs d'apprentissage

  • Expliquer pourquoi un résultat computationnel dépend des données, du code, de l'environnement et des versions
  • Créer et verrouiller un environnement conda à partir d'un fichier environment.yml épinglé
  • Versionner un projet avec Git en excluant les données volumineuses et les secrets
  • Organiser un projet reproductible et vérifier l'intégrité des fichiers par empreinte SHA-256
  • Distinguer le rôle de Snakemake et de Nextflow dans l'orchestration d'un pipeline

conda 24.xGit 2.40+JupyterLab 4.xSnakemake 8.x

Prérequis

Cette section ouvre le module 0 et ne suppose aucun prérequis formel. Il suffit de savoir ouvrir un terminal, d'avoir écrit un script Python de quelques lignes et de distinguer un fichier d'un dossier. Les conventions de notation du cours sont posées ici ; la section 1.1 les applique aux séquences. Aucun outil ne doit être installé pour lire la leçon : les commandes sont expliquées avant d'être utilisées.

Introduction

Un projet de bioinformatique enchaîne contrôle qualité, alignement, appel de variants, annotation, tableaux et figures. Chaque étape mobilise un logiciel, une version et des paramètres. Le résultat final, par exemple la liste des variants d'une souche d'Escherichia coli, est donc le produit d'une chaîne technique autant que d'une hypothèse biologique. Si un maillon change, le résultat peut changer sans que la biologie ait changé.

Le problème n'est pas théorique : un script qui fonctionne aujourd'hui peut échouer six mois plus tard, parce qu'une bibliothèque a été mise à jour, parce que le compilateur diffère, ou parce que le fichier nommé latest a changé. Un sondage publié par Nature en 2016 auprès de 1 576 chercheurs indiquait que 70 % d'entre eux avaient déjà échoué à reproduire le résultat d'un collègue et 50 % à reproduire le leur (Baker, 2016). La biologie computationnelle n'y échappe pas : la crise de réplication est aussi une affaire de versions, de chemins et de fichiers.

La reproductibilité computationnelle est la capacité à obtenir le même résultat à partir des mêmes données, du même code et du même environnement. Elle se distingue de la réplication, qui produit de nouvelles données, et de la simple répétition, effectuée par la même personne sur la même machine. Cette section présente les fondations : un environnement isolé et épinglé, un historique Git, des notebooks exécutés dans l'ordre, une arborescence claire, des empreintes de fichiers et une première idée des pipelines. L'objectif n'est pas d'installer tous les outils, mais de comprendre ce qui rend un résultat vérifiable.

Théorie

Pourquoi un résultat computationnel n'est pas reproductible par défaut

Un calcul dépend de plus de choses qu'il n'y paraît. La version de Python, celle de chaque bibliothèque, les versions des bibliothèques C sous-jacentes, le système d'exploitation, l'architecture du processeur, les variables d'environnement, la locale et le chemin des fichiers peuvent modifier un résultat ou empêcher son obtention. Les gestionnaires de paquets installent par défaut la dernière version compatible ; deux installations à deux dates différentes ne sont donc pas équivalentes, même avec la même commande.

Les données sont l'autre source d'instabilité. Un identifiant comme NC_000913.3 désigne une version précise, mais un fichier téléchargé « au dernier moment » ou un répertoire data/latest/ ne désignent rien de stable. Il faut enregistrer la provenance : URL, accession, date de téléchargement, licence et empreinte. Un résultat sans provenance n'est pas vérifiable, même s'il est numériquement correct.

Enfin, certains calculs sont intrinsèquement non déterministes : ordre d'exécution de threads, sommes flottantes, générateurs aléatoires non initialisés. On les maîtrise en fixant une graine, en imposant un ordre et en comparant avec une tolérance. La reproductibilité stricte, au bit près, n'est pas toujours possible ; elle doit alors être remplacée par une tolérance explicite.

Formellement, on peut voir un résultat RR comme une fonction :

R=f(D,C,E,θ,s)R = f(D, C, E, \theta, s)

où DD désigne les données, CC le code, EE l'environnement, θ\theta les paramètres et ss la graine. Reproduire un résultat, c'est fixer et documenter chacun de ces arguments. Cette écriture explique pourquoi un README qui se contente de « lancer script.py » est insuffisant : il manque EE, souvent θ\theta, et parfois la version de DD.

Environnements conda et épinglage

Un environnement est un ensemble cohérent d'interpréteurs, de bibliothèques et de binaires. venv isole les paquets Python, pas les outils non Python comme samtools ou fastp ; conda gère les deux, y compris les binaires. Les canaux conda-forge et bioconda couvrent l'essentiel ; bioconda dépend de conda-forge, à placer avant lui.

Le fichier environment.yml décrit l'environnement. Un exemple minimal et épinglé :

name: bioinfo
channels:
  - conda-forge
  - bioconda
dependencies:
  - python=3.11.9
  - numpy=1.26.4
  - pandas=2.2.3
  - biopython=1.84
  - samtools=1.19.2
  - snakemake-minimal=8.10.0

Les numéros de version de cet exemple sont illustratifs ; vérifiez leur disponibilité dans les canaux avant de créer l'environnement. La commande conda env create -f environment.yml crée l'environnement, et conda run -n bioinfo python script.py exécute un script sans activer l'environnement dans le shell courant. L'épinglage consiste à écrire une version complète, samtools=1.19.2, et non un intervalle, samtools>=1.19. Un intervalle autorise toute version future : il documente une compatibilité, pas un résultat.

Un fichier épinglé ne suffit pas : le solveur choisit aussi les dépendances transitives et les builds. Pour figer l'ensemble, exportez la liste effective : conda env export --no-builds conserve les dépendances transitives sans les builds, conda env export --from-history ne garde que les paquets demandés explicitement, et conda list --explicit produit des URL et des hachages. Versionnez un verrou (lock file) généré par conda-lock ou par l'export explicite ; il est spécifique à une plateforme (linux-64, osx-arm64, win-64).

Git : historique, branches et fichiers exclus

Git enregistre des instantanés du projet. Un commit associe un ensemble de fichiers à un message, un auteur et une date ; l'historique permet de retrouver l'état exact du code utilisé pour une figure. Les commits doivent être petits et thématiques : « ajoute le contrôle qualité » est plus utile que « modifications ». Une branche isole une expérience ; une étiquette (tag) marque une version publiée, par exemple v1.0.0, et permet de citer un état précis dans un article.

Deux règles évitent la plupart des accidents. D'abord, on ne versionne pas les données brutes volumineuses, les résultats régénérables, les fichiers de cache ni les secrets. Un fichier FASTQ de plusieurs gigaoctets alourdit définitivement l'historique ; un fichier .env peut contenir une clé d'API. Ensuite, on versionne ce qui décrit le calcul : code, environment.yml, fichiers de configuration, petits jeux de données de test, manifeste d'empreintes et README. Pour les fichiers volumineux mais nécessaires, Git LFS stocke un pointeur dans le dépôt et les données ailleurs ; DVC et DataLad remplissent un rôle analogue avec un stockage distant.

Un .gitignore explicite est plus sûr qu'un nettoyage tardif :

data/raw/
results/
*.bam
*.fastq.gz
.env
__pycache__/
.ipynb_checkpoints/

Si un fichier a déjà été ajouté, git rm --cached fichier le retire de l'index sans effacer la copie locale, mais il reste dans l'historique. Pour un secret, il faut le révoquer et réécrire l'historique, pas seulement supprimer le fichier. Enfin, notez le commit dans les rapports : git rev-parse --short HEAD et git describe --always --dirty indiquent l'état exact, y compris les modifications non validées.

Jupyter : noyaux et état caché

Un notebook Jupyter est un document JSON qui mélange code, sorties et texte. Le noyau (kernel) est le processus qui exécute le code et il est lié à un environnement précis. Ouvrir un notebook avec le noyau global alors que l'environnement bioinfo contient Biopython produit une erreur, ou pire, une autre version sans que l'on s'en aperçoive. La commande python -m ipykernel install --user --name bioinfo --display-name "Python (bioinfo)" enregistre le noyau de l'environnement ; jupyter kernelspec list vérifie les noyaux disponibles.

Le piège principal est l'état caché. Les cellules peuvent être exécutées dans un ordre arbitraire, réexécutées ou interrompues ; le notebook affiché ne correspond alors pas à une exécution linéaire. Avant de conclure, redémarrez le noyau et exécutez toutes les cellules (Restart Kernel and Run All). Pour produire un artefact vérifiable, jupyter nbconvert --to notebook --execute --inplace analyse.ipynb exécute le notebook dans un noyau propre et échoue si une cellule lève une exception. papermill paramètre et rejoue un notebook. Enfin, nettoyez les sorties avant de versionner (nbstripout ou jupyter nbconvert --clear-output --inplace) et préférez un script ou un fichier jupytext pour les parties stables : les diffs JSON sont illisibles.

Organiser un projet

Une arborescence stable vaut mieux qu'une convention personnelle. Les données brutes sont immuables : on les lit, on ne les écrase jamais. Les données dérivées sont régénérables et peuvent être supprimées. Les résultats sont des sorties, pas des sources.

projet-demo/
├── README.md
├── environment.yml
├── data/
│   ├── raw/
│   │   └── demo.fastq
│   └── checksums.sha256
├── code/
│   └── 01_qc.py
├── rules/
│   └── Snakefile
├── notebooks/
│   └── exploration.ipynb
├── results/
│   └── qc/
├── logs/
└── tests/
    └── test_checksums.py

Le README doit répondre à quelques questions : quelle est la question biologique, d'où viennent les données (URL, accession, date, licence), quel environnement utiliser, quelles commandes lancer, où sont les résultats attendus et comment les vérifier. Ajoutez la date de dernière exécution et la version du code. Un manifeste checksums.sha256 contient une empreinte par fichier ; il permet de détecter une troncature, un téléchargement incomplet ou une modification accidentelle. Les chemins doivent être relatifs à la racine du projet, jamais absolus : un chemin C:\Users\... ne survit pas au changement de machine.

Pipelines : Snakemake et Nextflow

Dès que plusieurs étapes s'enchaînent, un script monolithique devient difficile à reprendre et à auditer. Un pipeline déclare des étapes et leurs dépendances sous forme de graphe orienté acyclique (DAG, directed acyclic graph). Chaque nœud est une règle ou un fichier, chaque arête une dépendance. Le moteur peut alors calculer l'ordre, ne réexécuter que les étapes invalidées et conserver les commandes exécutées.

Snakemake s'écrit en Python. Une règle déclare input, output et shell ; les motifs (wildcards) généralisent les noms de fichiers ; rule all désigne les cibles finales. Nextflow utilise un langage dédié de type Groovy, des processus et des canaux (channels) ; il vise les clusters et les clouds. Les deux gèrent la reprise, les conteneurs et les environnements conda, et les deux produisent un graphe visualisable.

CritèreSnakemakeNextflow
LangagePythonDSL de type Groovy
Modèlerègles et fichiersprocessus et canaux
Reprisecache par fichier et par code-resume, hachage des tâches
Conteneurscontainer: ou environnements condaDocker, Singularity, Apptainer
ÉcosystèmeBioconda, scripts Pythonnf-core, HPC et cloud

Un pipeline ne rend pas un résultat reproductible par magie. Il rend les dépendances explicites et journalise les commandes, ce qui est déjà beaucoup. Il faut encore épingler les environnements, versionner les règles et vérifier les sorties. La reprise incrémentale elle-même repose sur une convention : un fichier de sortie est valide si ses entrées n'ont pas changé et si le code de la règle n'a pas changé. Si cette convention est fausse, le pipeline réutilise un résultat périmé.

Exemple travaillé à la main

Prenons quatre règles : qc_raw lit data/raw/demo.fastq et écrit results/qc/raw.html ; trim lit le même fichier et écrit data/processed/demo.trim.fastq ; qc_trim lit ce fichier filtré et écrit results/qc/trim.html ; checksum lit le fichier filtré et écrit results/checksums.sha256.

Les dépendances sont donc : qc_raw et trim dépendent de la donnée brute ; qc_trim et checksum dépendent de trim. Un ordre topologique possible est qc_raw, trim, checksum, qc_trim : les deux premières règles sont prêtes, le départage alphabétique place qc_raw avant trim, puis checksum et qc_trim deviennent prêtes dans cet ordre. L'ordre trim, qc_raw, qc_trim, checksum est aussi valide.

Supposons maintenant que l'on change le seuil de qualité de fastp dans trim. Les entrées de trim n'ont pas changé, mais son code a changé : trim doit être réexécutée, ainsi que qc_trim et checksum, qui dépendent de sa sortie. qc_raw reste valide. Si l'on modifie seulement la mise en forme du rapport de qc_trim, seule cette règle est invalidée. Si le fichier brut change, les quatre règles le sont. Cette analyse à la main est exactement ce que le moteur calcule ; la faire soi-même aide à interpréter un --dry-run et à repérer une dépendance oubliée.

Figure 0.1.1. Graphe de dépendances du projet de démonstration : la donnée brute alimente le contrôle qualité et le filtrage ; le filtrage alimente le contrôle qualité final et l'empreinte.

Implémentation

Le contrôle d'intégrité tient en quelques lignes. L'empreinte SHA-256 est une fonction de hachage : elle transforme un contenu de longueur quelconque en 256 bits, affichés en 64 caractères hexadécimaux. Deux contenus différents produisent presque toujours deux empreintes différentes, et un contenu donné produit toujours la même empreinte. Le code suivant calcule l'empreinte d'un texte et fabrique une ligne de manifeste compatible avec l'outil sha256sum :

Empreinte SHA-256 et manifeste
SortieCtrl+Entrée pour exécuter · 10 s max
Aucune sortie. Exécutez le code pour voir le résultat.

Le code s'exécute dans votre navigateur (Web Worker isolé). Il n'a pas accès au serveur ; l'accès réseau est désactivé pendant l'exécution. Le premier clic sur « Exécuter » télécharge Pyodide depuis jsDelivr (plusieurs mégaoctets, version 0.26.4 épinglée) ; rien n'est chargé avant ce clic.

La sortie vérifiée le 2026-11-01 est :

empreinte : bbe388877eb03743000b2511e1bab7cc24be7bace54c9032b750522e730625f3
manifeste : bbe388877eb03743000b2511e1bab7cc24be7bace54c9032b750522e730625f3  data/raw/demo.fastq

Deux espaces séparent l'empreinte du chemin ; c'est le format attendu par sha256sum -c. Le calcul porte sur des octets, pas sur des caractères : l'encodage UTF-8, les fins de ligne et les espaces comptent. Un fichier réécrit en mode texte sous Windows peut passer de \n à \r\n et changer d'empreinte sans que son contenu visible change. Pour un fichier, on lit en binaire et par blocs, afin de ne pas charger un FASTQ de plusieurs gigaoctets en mémoire :

from hashlib import sha256
from pathlib import Path
 
def sha256_file(path: str | Path) -> str:
    digest = sha256()
    with open(path, "rb") as handle:
        for chunk in iter(lambda: handle.read(1024 * 1024), b""):
            digest.update(chunk)
    return digest.hexdigest()

Les commandes ci-dessous montrent l'enchaînement minimal. Elles sont illustratives et doivent être adaptées au système : sha256sum existe sous Linux et macOS, tandis que PowerShell propose Get-FileHash -Algorithm SHA256.

set -euo pipefail
conda env create -f environment.yml
conda run -n bioinfo python -c "import sys; print(sys.version)"
git init
git add README.md environment.yml code/ rules/
git commit -m "Initialise le projet et épingle l'environnement"
git switch -c analyse-qc
git tag -a v0.1.0 -m "Version analysée"
sha256sum -c data/checksums.sha256

Pour un pipeline, la règle Snakemake suivante déclare ses entrées, sa sortie, son environnement et sa commande. Le moteur déduit le graphe et ne réexécute la règle que si nécessaire :

rule all:
    input:
        "results/qc/trim.html",
        "results/checksums.sha256"
 
rule trim:
    input:
        "data/raw/demo.fastq"
    output:
        "data/processed/demo.trim.fastq"
    conda:
        "env/trim.yml"
    shell:
        "fastp -i {input} -o {output} -q 20 -l 50"

On vérifie le plan avec snakemake -n --cores 1, puis on exécute avec snakemake --cores 4 --use-conda. L'équivalent Nextflow sépare le processus et le flux :

process TRIM {
    input:
    path reads
    output:
    path "demo.trim.fastq"
    script:
    """
    fastp -i ${reads} -o demo.trim.fastq -q 20 -l 50
    """
}
 
workflow {
    Channel.fromPath("data/raw/demo.fastq") | TRIM
}

Nextflow se lance avec nextflow run main.nf -resume ; -with-docker ou -with-singularity impose un conteneur, et -with-dag dag.html produit le graphe. Les points critiques sont toujours les mêmes : épingler les environnements, hacher les octets et non le texte affiché, vérifier le code de retour des commandes, journaliser les versions et ne jamais conclure à partir d'un notebook dont l'état d'exécution est inconnu.

Étude de cas

Nous prenons un projet de démonstration synthétique, projet-demo, construit pour cette leçon. Il ne contient qu'une lecture FASTQ, mais sa structure est celle d'un vrai projet : données brutes, code, règles, résultats, tests, manifeste et documentation.

projet-demo/
├── README.md
├── environment.yml
├── data/
│   ├── raw/
│   │   └── demo.fastq
│   └── checksums.sha256
├── code/
│   └── 01_qc.py
├── rules/
│   └── Snakefile
├── results/
│   └── qc/
└── tests/
    └── test_checksums.py

Le fichier data/raw/demo.fastq contient exactement ces quatre lignes, terminées par un saut de ligne :

@r1
ACGTACGT
+
IIIIIIII

Le fichier est écrit en binaire pour fixer les fins de ligne :

from pathlib import Path
 
raw = "@r1\nACGTACGT\n+\nIIIIIIII\n"
Path("data/raw/demo.fastq").write_bytes(raw.encode("utf-8"))

Son empreinte est bbe388877eb03743000b2511e1bab7cc24be7bace54c9032b750522e730625f3. Le manifeste data/checksums.sha256 contient donc :

bbe388877eb03743000b2511e1bab7cc24be7bace54c9032b750522e730625f3  data/raw/demo.fastq

La question est la suivante : peut-on rattacher les résultats aux données brutes et les régénérer ? Nous procédons en cinq étapes.

  1. Écrire le fichier brut en binaire, pour fixer les fins de ligne, puis calculer son empreinte.
  2. Comparer cette empreinte à celle du manifeste avec sha256sum -c data/checksums.sha256.
  3. Modifier volontairement une base du fichier brut et relancer la vérification.
  4. Restaurer le fichier d'origine et vérifier de nouveau.
  5. Exécuter le pipeline depuis un répertoire results/ vide et comparer les sorties.

Les résultats attendus sont les suivants. À l'étape 2, la commande affiche data/raw/demo.fastq: OK et renvoie le code 0. À l'étape 3, elle affiche FAILED et renvoie le code 1, ce qui interrompt un script protégé par set -e. À l'étape 4, elle affiche de nouveau OK. Ces sorties sont illustratives ; elles correspondent au format standard de sha256sum.

L'interprétation demande de la prudence. Une empreinte détecte une modification accidentelle, une troncature ou un téléchargement incomplet. Elle ne prouve pas l'authenticité : un adversaire qui modifie le fichier peut recalculer le manifeste. Pour aller plus loin, on signe le manifeste, on le versionne dans Git ou on l'enregistre dans un système horodaté. Une empreinte ne prouve pas non plus la validité biologique : un fichier corrompu de façon cohérente, ou simplement faux, peut avoir une empreinte parfaitement correcte. Enfin, le manifeste doit couvrir les données brutes et, si possible, les sorties intermédiaires que l'on souhaite comparer.

Erreurs fréquentes

Quiz

  1. Question 1.Que garantit un fichier environment.yml correctement épinglé ?
  2. Question 2.Quel fichier faut-il versionner dans Git ?
  3. Question 3.Un notebook affiche un résultat, mais les cellules ont été exécutées dans le désordre. Que faire ?
  4. Question 4.À quoi sert une empreinte SHA-256 dans un projet ?
  5. Question 5.Dans un pipeline, la reprise incrémentale consiste à…
Répondez à toutes les questions pour valider.

Exercices

Exercice guidé0.1-e1

Énoncé

Écrire parse_environment_yml(text) qui analyse le texte d'un environment.yml minimal et renvoie un dictionnaire avec name (chaîne ou None), channels (liste), dependencies (liste des spécifications de premier niveau) et unpinned (liste des spécifications sans version exacte). Ignorer les lignes vides, les commentaires et le bloc imbriqué sous - pip:. Une spécification est épinglée si elle contient = ou == comme contrainte d'égalité, mais pas >=, <=, != ou ~=. Les guillemets autour des valeurs sont retirés. L'ordre d'entrée est conservé.

Comment vérifier votre travail

Les tests se trouvent dans exercises/module-0/0.1-introduction-reproductibilite/test_0_1.py. Depuis la racine du dépôt :

python -m pytest exercises/module-0/0.1-introduction-reproductibilite -q

Corrigé verrouillé

Le corrigé est chargé uniquement après confirmation. Il n'est pas présent dans la page avant cette action.

Exercice approfondi0.1-e2

Énoncé

Écrire sha256_file(path) qui calcule l'empreinte SHA-256 d'un fichier en le lisant par blocs de 1 Mio, et verify_checksums(manifest, root=None) qui vérifie une liste de dictionnaires {"path": ..., "sha256": ...}. La fonction renvoie {"ok": bool, "checked": int, "missing": [...], "mismatched": [...]}. root sert de répertoire de base pour les chemins relatifs. Un fichier absent ou illisible va dans missing ; une empreinte différente va dans mismatched. checked compte les fichiers lus. Comparer les empreintes sans tenir compte de la casse. Un manifeste vide est valide.

Comment vérifier votre travail

Les tests se trouvent dans exercises/module-0/0.1-introduction-reproductibilite/test_0_1.py. Depuis la racine du dépôt :

python -m pytest exercises/module-0/0.1-introduction-reproductibilite -q

Corrigé verrouillé

Le corrigé est chargé uniquement après confirmation. Il n'est pas présent dans la page avant cette action.

Mini-projet0.1-projet

Énoncé

Mini-projet : ordonnancement d'un pipeline. Écrire topological_order(rules) et pipeline_stages(rules). rules associe un nom de règle à la liste de ses dépendances. topological_order renvoie tous les noms, dépendances avant dépendants, avec départage alphabétique déterministe. pipeline_stages renvoie une liste d'étapes, chaque étape contenant les règles dont toutes les dépendances sont dans les étapes précédentes, triées alphabétiquement. Une dépendance inconnue ou un cycle lève ValueError. Un dictionnaire vide renvoie []. Les doublons de dépendances sont ignorés.

Comment vérifier votre travail

Les tests se trouvent dans exercises/module-0/0.1-introduction-reproductibilite/projet/test_projet_0_1.py. Depuis la racine du dépôt :

python -m pytest exercises/module-0/0.1-introduction-reproductibilite/projet -q

Corrigé verrouillé

Le corrigé est chargé uniquement après confirmation. Il n'est pas présent dans la page avant cette action.

Résumé

  • Un résultat reproductible se décrit par ses données, son code, son environnement, ses paramètres et sa graine.
  • conda isole les outils Python et non Python ; un environment.yml épinglé doit être complété par un verrou des dépendances transitives.
  • Git versionne le code, la configuration et la documentation, jamais les données volumineuses ni les secrets.
  • Une branche isole une expérience ; une étiquette fixe la version citée dans un rapport.
  • Un notebook n'est fiable qu'après un redémarrage du noyau et une exécution complète dans l'ordre.
  • Les données brutes restent immuables, les résultats sont régénérables, et le README documente la provenance.
  • Un manifeste SHA-256 détecte les modifications accidentelles ; il ne prouve ni l'authenticité ni la validité biologique.
  • Snakemake et Nextflow explicitent le graphe, la reprise et les conteneurs, sans dispenser d'épingler les environnements.

Références

  • Baker, M. (2016). 1,500 scientists lift the lid on reproducibility. Nature, 533, 452–454.
  • Sandve, G. K., Nekrutenko, A., Taylor, J., & Hovig, E. (2013). Ten simple rules for reproducible computational research. PLoS Computational Biology, 9(10), e1003285.
  • Mölder, F. et al. (2021). Sustainable data analysis with Snakemake. F1000Research, 10, 33.
  • Di Tommaso, P. et al. (2017). Nextflow enables reproducible computational workflows. Nature Biotechnology, 35(4), 316–319.
  • The Turing Way Community (2022). The Turing Way: A Handbook for Reproducible, Ethical and Collaborative Data Science. https://the-turing-way.netlify.app
  • Git. Git Documentation. https://git-scm.com/doc
  • conda. conda Documentation. https://docs.conda.io/