# cutevariant 2.0
## Sequence d'utilisation
```mermaid
sequenceDiagram
User->>GUI: Crée une base
GUI->>DATA: Crée le dossier du project
User->> GUI: Import de(s) VCF(s)
GUI->>DATA: Crée les fichiers parquets
User->> GUI: Créer un project avec des patients
GUI->>DATA: Créér une entrée dans la table 'project'
User--> GUI: choisi un project
User--> GUI: choisi des filtres
User--> GUI: choisi des fields
User->>GUI: Lance une requete
GUI->>DATA: execute une requete duckdb
DATA->>GUI: renvoie un polars.DataFrame
GUI->>User: montre les variants dans un tableau
User->>GUI: Interagit (flags, class..)
GUI->>DATA: Sauvegarde en sqlite
```
Avant l'étape 'créer un projet avec des patients', tout le reste est disponible directement en ligne de commande. Ainsi, cutevariant est intégrable dans un pipeline bioinfo standard. Et ce afin d'éviter à l'utilisateur final la phase d'import des vcf, qui de toutes façons est fait une fois pour toutes.
# TEST
## Cutevariant Projet
- les fichiers parquet
- les structures des fichiers
- la base de donnée duckdb
- Definir le VQL
- Definir l'API
# TEST
## Schéma de donnée
### Organisation d'un dossier
```
- myDB/
- variants.parquet
- project.db
- genotypes/
- sample1.parquet
- sample2.parquet
```
### Fichier parquets
```
variants.parquet
- hash
- chr
- pos
- ref
- alt
- type : sv, del ...
- len :
sample1.parquet
- hash
- sample
- gt
- ad
- dp
- gq
```
### Tables
project.db
#### variants duckdb
Contient les variants uniques trouvés dans l'ensemble des vcf importés
- **hash** (uint64): Le hash du variant sur (chr,pos,ref,alt)
- **chr** (string): nom du chromsome
- **pos** (uint64): position du variant sur le chromosome
- **ref** (string): allèle de référence
- **alt** (string): allèle alternatif
- **count_hetero** (uint32): Nombre de fois vue hétéro
- **count_homo** (uint32): Nombre de fois vue homo
- **classification** (uint8) : Classification ACMG
- **tags** (list) : Une liste de [warning, attention]
- **comment** (varchar) : Un commentaire associé au variant
- **artefact** (bool): truc -> pb: si je comprends bien cette table ne doit pas être mise à jour trop souvent si ?
#### Analyses
- **name** (varchar) Nom de l'analyse
- **samples** (list) : listes des patients
- **status** (int): [nouveau, en cours, validé]
- **created** (date)
- **validated** (date)
- **author** (varchar)
- **comment**
#### Samples
- **id**
- **name** : sample name
- **file** : parquet file
- **alias**: alias 'mother, father ..'
- **affected** : index ou pas
- **father_id** :
- **mother_id**
- **tags**
- **comment**
- **hpo**
#### Logs
- author
- table
- table row
- field
- before
- after
#### sampleN.parquet
Contient les genotypes d'un patient pour chaque variants. Dans l'ancienne version, le nombre de colonne était dynamique. On fixe tout. Tant pis pour les autres colonnes.
Pas d'accord. On pourrait faire:
sampleN.gt.parquet avec les champs ci-dessous
- **hash** (uint64): hash(chr,pos,ref,alt)
- **sample** (string): nom du patient
- **gt** (uint8): 1=hetero, 2=homo, 0=ref, 3=unknown
- **dp** (uint32): profondeur
- **ad** (uint32): nombre de fois vu ref
- **gq** (uint32): Qualité
Et autant de fichiers parquet, pour chaque génotype, que de champs trouvés dans le VCF.
Exemple, si le vcf contient les champs ac, cc, et vaf (y a que vaf qui existe pour sûr):
sampleN.gt.parquet
sampleN.ac.parquet
sampleN.cc.parquet
sampleN.vaf.parquet
Si le champ n'est pas présent dans l'échantillon, il sera considéré comme NULL.
Je propose également un plugin qui affiche, pour chaque échantillon, la liste des champs qu'il contient (GT, DP, AD, GQ, etc) en se basant sur ce qui a été importé.
#### annotation.parquet
Contient des annotations par variants
- **hash** (uint64): hash(chr,pos,ref,alt)
- **field1** (string): custom
- **field2** (uint8): custom
- **field3** (uint32): custom
### Construction d'une base a partir d'un VCF
A partir d'un fichier VCF, on extrait les variants (chr,pos,ref,alt), les genotypes (gt,ad,gq) et les annotations (field1,field2) pour construire des fichiers parquet. La transformation peut se faire avec duckdb ou pola.rs
```mermaid
flowchart
input.vcf-->input.parquet
input.parquet-->sample1.parquet
input.parquet-->sample2.parquet
input.parquet--->variant.parquet
```
- La construction du fichier `variants.parquet` se fait en duckdb :
```sql
SELECT FIRST(hash),chr,pos,ref,alt, SUM(count_hetero), SUM(count_homo)
FROM (
SELECT * variant.parquet UNION SELECT * FROM var.parquet
) GROUP BY chr,pos,ref,alt
```
- Le fichier `gen.parquet` est juste ajouté avec les autres genotypes
- Le fichier `ann.parquet` est ajouter aux annotations avec un DISTINCT
### Base duckdb
utilisé pour les interactions utilisations en multiusers. Pas sur que Duckdb soit bien pour cette usage.
```
Metadata {
string key
boolean values... Boolean ?
}
Tags {
int id
string tag
string type
}
Classifications {
int id
name classification
}
Variants {
int hash
int classification
string tags
string comment
string author
date create
}
Samples {
int id
string name
string alias
bool affected
int father_id
int mother_id
int status
string tags
string comment
string author
}
Genotypes {
int hash
int sample_id
int status
string tags
string author
}
Preset {
}
Project {
int id
string name
list samples
date created
date modified
string author
}
```
### VQL
Nouvelle syntaxe pour les filtres sur les samples
```sql
WHERE sample[{selector}].gt > 1
```
Le selector peut etre :
- Le nom du sample : `sample["boby"]`
- une liste : `sample[("father","mother)]` !!! Se traduit par un OU ou un ET ? Logiquement je dirais un ET mais un OU n'est pas absurde non plus...
- L'alias du sample : `sample["father"]`
- Une expression : `sample[affected==true AND name=='sacha']`
- Tous ou any : `sample[ANY]`
SELECT chr, pos, ref, alt , c.gt, b.gt, s.affected
FROM variants v
JOIN charles c ON c.id = v.id
JOIN boby n ON b.id = b.id
JOIN samples on s.name = 'boby' OR s.name = 'charles'
WHERE c.gt > 0 AND s.affected = True
fields : []
filters: []
{
fields : ["chr","pos"],
filters: ["AND"]
}
#### Alias
```bash
# Retourne la liste des alias définis
ALIAS LIST
# Retourne la liste des alias qui correspondent à l'expression
ALIAS LIST LIKE '(mo|fa)ther'
# Si l'alias existe déjà, affiche un warning et refuse d'écraser
ALIAS SET "NA12878" AS "father"
# Si l'alias existe déjà, écraser
ALIAS SET "NA12878" AS "mother" OVERWRITE
```
Les alias ne seront pas forcément simples à manipuler...
Exemple:
J'ai déjà délcaré que 'father' est un alias de 'NA12878'.
Maintenant, j'aimerais déclarer que 'father' est un alias de 'NA12877'. Il faut donc s'assurer que 'father' n'est plus un alias de 'NA12878'.
De même, si 'father' est un alias de 'NA12878', il faut s'assurer que je ne peux pas déclarer 'mother' comme alias de 'NA12878'. Ou que si je le fais, 'mother' devient effectivement un alias de 'NA12878', mais 'father' n'est plus un alias de rien du tout.
Contre ce problème, je propose donc la syntaxe suivante:
```bash=
ALIAS ADD "father" # Renvoie une c. si un échantillon s'appelle comme ça !
ALIAS ADD "mother"
ALIAS ADD "index"
ALIAS SET "NA12878" AS "index" # Si index était déjà un alias pour un autre échantillon, il fait maintenant référence à NA12878
```
#### Types de retour des requêtes VQL
Commande SELECT: une liste de clés-valeurs (un tableau). Une ligne par résultat, une colonne par champ sélectionné (standard).
Commande ALIAS: une liste de clés-valeurs également. Clé: l'alias. Valeur: la valeur représentée par l'alias.
Commande CREATE: en cas de succès, renvoie une liste des projets existants ainsi que le nouveau projet. En cas d'échec, renvoie le message d'erreur qui a empêché la création du projet.
Selon le type de retour, la variant view affichera une vue adaptée dans l'onglet sélectionné.
Pour une commande SELECT, ce sera une TableView avec une ligne par résultat, une colonne par champ sélectionné.
Pour une commande ALIAS, ce sera une TableView avec une ligne par alias existant, et de colonnes (l'alias, et sa valeur correspondante)
Pour une commande CREATE, ce sera une ListView avec une ligne par projet existant. En cas d'échec de la création du projet, elle n'affichera que ceux qui ont déjà été créés, en plus d'une MessageBox d'erreur.
## Diagramme de classes
### Plugins
- Un plugin s'abonne à des clefs (fields, filters, project).
- le mainwindow rafraîchit les plugins quand la clef change
- Les plugins peuvent demander de rafraîchir les autres plugins --> Pas la peine selon moi
```mermaid
classDiagram
class AbstractPlugin{
List subscribes$
Signal send_refresh(state:dict)
void on_refresh(state:dict)
void on_register(mainwindow)
void on_open(project_path)
}
class MainWindow{
+dict plugins
-register_plugins()
-refresh_plugins()
-refresh_plugin(plugin)
-find_plugins()
}
class State {
list fields
dict filters
string project
string variant_hash
}
```
```python=
# Fichier: mon_plugin.py
def register_plugin(state: State):
# On instancie le plugin dans la fonction register. Comme ça, mainwindow n'a pas besoin de savoir comment créer ce plugin en particulier.
plugin_instance=MonPlugin()
return plugin_instance, {
"on_refresh":plugin_instance.on_refresh,
"on_open_project":plugin_instance.on_open_project,
"on_fields_changed":plugin_instance.on_fields_changed,
"on_filters_changed":lambda:None
}
# Fichier: mainwindow.py
def register_plugins():
for plugin in find_plugins():
self.plugins[plugin.name]=plugin.register_plugin()
```
OU BIEN
```python=
class State:
def __init__(self,fields:List[str], filters:dict, project:str):
self.fields=fields
self.filters=filters
self.project=project
self.subscriptions={"fields":[],"filters":[],"project":[]}
self.plugins=dict()
def register_plugin(self,plugin:BasePlugin):
# On demande au plugin à quoi il veut s'abonner
subscriptions=plugin.subscriptions()
for sub in subscriptions:
self.subscriptions[sub].append(plugin)
self.plugins[plugin.name()]=plugin
# Peut-être un peu de répétitions...
def change_fields(self,new_fields,caller:BasePlugin=None):
for plugin in self.subscriptions["fields"]:
if plugin is not caller:
plugin.on_fields_change()
def change_filters(self,new_filters,caller:BasePlugin=None):
for plugin in self.subscriptions["filters"]:
if plugin is not caller:
plugin.on_filters_change()
class BasePlugin:
def __init__(self,window_state:State):
self.window_state=window_state
self.window_state.register_plugin(self)
# Une méthode qui doit être appelée par les sous-classes pour changer les champs
def change_fields(self,new_fields):
self.window_state.change_fields(new_fields,caller=self)
# Une méthode virtuelle que les sous-classes doivent implémenter si elles veulent réagir
def on_fields_changed(self,new_fields):
pass
class FieldsPlugin(BasePlugin):
def __init__(self,window_state:State):
super().__init__(window_state)
self.my_gui.my_widget.whatever_signal.connect(self.change_fields)
def on_fields_changed(self,new_fields):
self.my_gui.my_widget.update_fields(new_fields)
def name(self):
return "fields"
def subscriptions(self):
return ("fields",)
```
```python=
class MainWindow:
state = {analyse, filter, fields}
def registerPlugin(plugin):
plugin.stateChanged.connect(setState)
def setState(sender):
for plugin in plugins:
if plugin != sender && plugin.subsbribe:
plugin.refresh()
class Plugin:
def onClick():
self.stateChanged.emit(fields="fields", filters={})
def refresh():
sfsf
```
### Reader
- Un Reader prend un fichier en entrée et produit des fichier parquet
```mermaid
classDiagram
class AbstractConvertor {
filename: str
to_variant()
to_genotype()
to_annotation()
}
```
#### Exemples
```python
from cutevariant import VcfReader
reader = VcfReader("test.vcf")
reader.to_variant("test.var.parquet")
reader.to_genotype("test.{sample}.parquet")
reader.to_annotation("test.ann.parquet")
reader.to_fields("test.fields.parquet")
from cutevariant import CsvWriter
write = CsvWriter("test.csv")
reader.write(query)
```
### VQL to SQL
- Le VQL est parsé pour extraire les fields, filters et project
```mermaid
flowchart TD
VQL-->fields
VQL-->filters
VQL--> project
project-->State
fields-->State
filters-->State
State-->QueryBuilder
QueryBuilder-->SQL
```
## Les wordsets sont des fichiers
```python
duckdb.sql("SELECT * FROM 'variant.parquet' WHERE ref IN (SELECT * FROM wordset WHERE ref=dsfs)")
filters = {
"gene": {"$in": "test.csv"}
}
```
## API
```python=
from cutevariant import Database
db = Database()
db.set_path("/home/NAS3")
db.set_name("exome")
db.set_assembly("hg19")
db.create()
db.connect()
db.close()
# importer des vcfs
db.import(VcfReader("file1.vcf"))
db.import(VcfReader("file2.vcf"))
db.import(VcfReader("file3.vcf"))
db.create_wordset()
db.get_wordsets(id=3)
db.drop_wordsets()
db.drop_wordsets(id=4)
# Crée une requete
query = {
"fields": ["chr","pos"],
"filters": [{"pos": 2424}],
"project": "run24",
"limit": 20,
"offset": 24
}
for variant in db.run_query(query):
print(variant["chr"])
db.save_query(query, "file.csv") ??
db.get_fields()
db.get_fields(table="variant")
db.get_fields(id=2432)
db.get_metadatas()
db.get_samples()
db.get_samples(name="boby")
db.get_projects()
db.get_projects(status="complete")
## Création d'un projet
db.create_project("run24", samples=["boby1","boby2"])
db.drop_project("run24")
db.drop_samples(affected: True)
db.update_variant(hash=24232544, tags="dsfs", comment="ceci est un commentaire" )
db.update_sample(234, tags="dsfs", comment="ceci est un commentaire" )
```python
class Plugin:
def fcxt(self):
# fcxt ? T'es bourré ou tu manques d'imagination ? :laughing:
self.send_state({"fields":[], "filters": []})
def get_field
def get_fields
def get_fields_by_category
get_fields(id=3)
get_fields()
get_fields(category='truic')
create
update
drop
```
## Variant view
La vue principale de la variant_view est un TabWidget. Quand une requête est exécutée, le résultat est affiché dans l'onglet actif.
Selon le type de requête, la variant view affiche une vue adaptée au type de retour (voir cette [section](#Types-de-retour-des-requêtes-VQL))
Idées en vrac
Si l'utilisateur choisit un BED, on lui génère le parquet associé dans le lac. L'utilisateur a alors le choix entre BED et parquet pour sélectionner ses annotations.
```
import cutevariant as cv
# Dossier core:
# - datalake.py
# - crud.py
# - vql.py
# Dossier reader
# Dossier writer
if __name__ == "__main__":
conn = cv.DataLake("/home/charles/Projets-Cutevariant/Maladies-rares")
# Crée le dossier du projet avec son architecture:
# Dossier genotypes:
# - sampleA.genotypes.parquet
# - sampleA2.genotypes.parquet
# - sampleB.genotypes.parquet
# - ...
# Dossier variants:
# - part0.parquet
# - part1.parquet
# - ....parquet
# - part255.parquet
# Dossier annotations:
# - gnomad.parquet
# - clinvar.parquet
# - ...
# Dossier aggregates:
# - variants.parquet (hash, chrom, pos, ref, alt, hom_count, het_count)
# Un fichier de base de données:
# - datalake.db
# - table variants: hash, favorites, comments, tags, ...
# - table samples: sample_hash, sample_name, father, mother, sex, affected, tags, ...
# - table projects: projectID, name, description, list_of_samples
# Si on ajoute un VCF:
# COPY (SELECT DISTINCT hash, chrom, pos, ref, alt FROM [variants.parquet, incoming.parquet]) TO .variants.parquet
# Rename .variants.parquet to variants.parquet
# Crée le dossier du projet avec son architecture telle que décrite plus haut
conn.init()
conn.import_vcf("/home/charles/Tests/test.vcf.gz")
# Exemple de requête traduite en DuckDB:
# """
# SELECT chr, pos, ref, alt , c.gt, b.gt, s.affected
# FROM aggregates/variants.parquet v
# JOIN genotypes/papa.parquet papa ON papa.id = v.id
# JOIN genotypes/maman.parquet maman ON maman.id = v.id
# JOIN genotypes/sacha.parquet sacha ON sacha.id = v.id
# JOIN samples on s.name = ‘boby’ OR s.name = ‘charles’
# JOIN annotations/bed/ENS_OK.parquet bed ON bed.chrom = v.chrom bed.start < v.pos AND bed.end > v.pos
# WHERE c.gt > 0 AND s.affected = True
# """
# SELECT chrom, sample['papa'].gt, pos, ref, alt, hom_count, het_count
# FROM 'Famille Rousseau'
# WHERE chrom = 'chr1' AND pos > 100000000 AND pos < 1000000000 AND hom_count > 0 AND sample['papa'].gt = 1 AND sample['$all'].gt = 1
df = conn.query(
fields=["chrom", "sample['papa'].gt", "pos", "ref", "alt", "hom_count", "het_count"],
filters={
"$and": [
{"chrom": {"$eq": "chr1"}},
{"pos": {"$gt": 100000000}},
{"pos": {"$lt": 1000000000}},
{"hom_count": {"$gt": 0}},
{"sample['papa'].gt": {"$eq": 1}},
{"sample['$all'].gt": {"$eq": 1}},
]
},
samples=["papa", "maman", "sacha"],
limit=50,
offset=0,
order_by=[("pos", "asc"), ("chrom", "desc")],
)
```
{
"fields":["chromosome","position","reference"]
}