---
# System prepended metadata

title: cutevariant 2.0

---

# 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"]
}