Calculs & affichages / Coding des nodes /
Check-list pour la création d'un node

Optimiser la création d'un node


Avant-propos

Amélioration du code :

Amélioration de l'affichage des Spinbox dans le dockable des paramètres

☐ Doc Spinbox


Debug : Couleurs incorrectes


Description

Check-list :

""" Renseigner les points marqués d'une case à cocher, effacer les cases au fur et à mesure.
    Exemple : création d'un node de type 'Opérateur', nommé 'mon_node'. A adapter, évidemment.
    IMPORTANT : CE FICHIER 'checklist.py' NE DOIT PAS ÊTRE MODIFIÉ. Seules les copies le seront.
    =================================================================================================== """
"""
                                            PRÉPARATION
    - Avant de commencer, imprimer ce fichier.                                                                  ☐

    - Décrire le node à créer (papier, tableau, traitement de texte, ...).
        - Sa fonction, qui définira le type (afficheur, opérateur, etc).                                        ☐
        - Le nombre d'entrées (fixe ou paramétrable).                                                           ☐
        - Le nombre de sorties (fixe ou paramétrable).                                                          ☐
        - Les widgets dans le node (ex : bouton dans le node 'Plots').                                          ☐
        - Les données nécessaires pour produire les signaux de sortie :
            - Signaux aux entrées.                                                                              ☐
            - Paramètres du dockable.                                                                           ☐
            - Paramètres étendus (Yaml).                                                                        ☐

    - Selon le type de node à créer, se placer dans le dossier /nodes/{type de node}. Ex: /nodes/operateurs/    ☐
        - Créer, si nécessaire, un nouveau type de node (un nouveau dossier).
    - Créer un sous-dossier portant le nom choisi. Ex: /nodes/operateurs/additionneur                           ☐
    - Y placer 2 fichiers. Ex : additionneur_seeder.yaml, additionneur_yaml.py                                  ☐
        - additionneur_seeder.yaml peut rester vide pour l'instant. Il sera modifié en cas de besoin.           ☐
        - additionneur_yaml.py, placer le code suivant :                                                        ☐

from nodes.yaml_parent import YamlParent


class YamlParams(YamlParent):
    def __init__(self, o_calc):
        super().__init__(o_calc, __file__)

    ===================================================================================================

                                           PC : POSTE DE CONTRÔLE

                     NODE DANS LE DOCKABLE DES NODES.
    - Copier ce fichier checklist.py, et renommer la copie -> /nodes/operateurs/mon_node/mon_node.py            ☐
    - Placer une image png de 64 x 64 au même emplacement. S'assurer de la transparence.                        ☐
    - Renseigner le dictionnaire d_datas ci-dessous : remplacer les 'xxxxx' par les bonnes valeurs.             ☐
    - Lancer le PC (main.py) -> Le nouveau node apparaît dans le dockable de nodes, onglet 'Opérateurs'.        ☐

                    AFFICHAGE DE CE NOUVEAU NODE DANS LA SCÈNE.
    - Depuis le PC : Créer ou ouvrir le graphe 'Creation_Node'.                                                 ☐
    - Le dossier '/backups/Creation_Node' doit être vide : Supprimer tous les éventuels dossiers et fichiers.   ☐
    - Lancer le PC, cliquer-glisser le nouveau node dans la scène.                                              ☐
    
                            LES ENTRÉES
    - Décommenter ci-dessous, dans le bloc 'Entrées et sorties', la partie 'Entrées'.                           ☐
    - Un choix est proposé selon que le nombre d'entrées est fixe ou paramétré.
        |_ Supprimer une des 2 propositions, adapter le code, notamment {label_pos}.                            ☐
    - Séparateurs : si le nombre d'entrées est paramétré, affecter {self.l_sep_inputs} dans __init__().         ☐

                            LES SORTIES
    - Décommenter ci-dessous, dans le bloc 'Entrées et sorties', la partie 'Sorties'.                           ☐
    - Un choix est proposé selon que le nombre de sorties est fixe ou paramétré :
        |_ Supprimer une des 2 propositions, adapter le code, notamment {label_pos}.                            ☐
    - Séparateurs : si le nombre de sorties est paramétré, affecter {self.l_sep_outputs} dans __init__().       ☐

                            VÉRIFICATION
    - Relancer le PC. Il doit y avoir le nombre d'entrées et de sorties spécifié (valeur par défaut si param).  ☐
    - Les couleurs des sorties s'affichent dans le dockable des paramètres. Vérifier leur persistance.          ☐
    
                    PARAMÈTRES FIXES, PROPRES AU NODE
    - Dans le bloc 'Paramètres statiques', décommenter ou supprimer :
        |_ La méthode fixed0_params() pour que ceux-ci soient AVANT les couleurs.                               ☐
        |_ La méthode fixed_params() pour que ceux-ci soient APRÈS les couleurs.                                ☐
    - Adapter les paramètres selon les besoins.
        |_ Vérifier leur persistance.                                                                           ☐
    
                    NOMBRE D'ENTRÉES ET DE SORTIES PARAMÉTRABLES
    - La @property ld_inputs() contient la variable 'nb_inputs'.
        |_ Le nom du paramètres est "Nombre d'entrées"
        |_ Ce paramètre DOIT EXISTER dans fixed0_params() ou fixed_params() : même orthographe, même casse.     ☐
    - Même remarque pour La @property ld_outputs() qui contient la variable 'nb_outputs'.                       ☐
    - Vérification : Relancer le PC pour s'assurer que les nombres d'entrées et de sorties sont pris en compte. ☐
        |_ Note  : Ils ne sont pas modifiés en direct car la méthode refresh() n'est pas encore codée.
        
        MODIFICATION EN DIRECT DU NOMBRE D'ENTRÉES ET DE SORTIES PARAMÉTRÉES
    - Décommenter la méthode refresh()                                                                          ☐
    - Vérification : Les nombres d'entrées et de sorties sont pris en compte en direct.                         ☐
    
                             SIGNAUX REÇUS AUX ENTRÉES
    - Décommenter la méthode my_params().                                                                       ☐
    - Connecter des nodes-serveurs aux entrées, avec un ou plusieurs signaux.                                   ☐
    - Vérification : On doit voir, dans le dockable des paramètres, tous les signaux, regroupés par entrées.    ☐
    - Les paramètres (dynamiques) sont ceux du dictionnaire fourni par my_params()
        |_ Par conséquent, leur nombre et leur type doivent être adaptés en fonction du rôle du node.           ☐
            |_ Remarque : Le 1er paramètre, 'Signal actif', booléen, a été inséré automatiquement. 

                            SIGNAUX DÉLIVRÉS AUX SORTIES
    - Décommenter la méthode my_signals().                                                                      ☐
    - Coder cette méthode, en pensant au nombre et aux caractéristiques des signaux de sortie.                  ☐
        |_ Pour chaque signal, le traitement peut utiliser toutes les informations à disposition.
        |_ Chaque signal est décrit par un tuple à 4 ou 5 valeurs.
    - Connecter un node-client à chaque sortie.                                                                 ☐
        |_ Afficher les paramètres de chacun des nodes-clients.                                                 ☐
        |_ Vérifier que les infos ont bien été transmises.                                                      ☐

=================================================================================================================

                                                  CALCULS

    - Décommenter la classe Calcul.                                                                             ☐
    - Le super-dictionnaire 'self.od_descr' contient une clé par signal à créer (signaux aux sorties).
        - Ce dictonnaire est créé par la méthode 'descr_signal()', appelée en boucle ...
            - ... un passage par signal d'entrée (actif ou pas).
        - Ce dictionnaire est ensuite utilisé pour effectuer les calculs et produire les signaux de sortie.
            - Ce traitement est effectué exclusivement par une des 2 méthodes : process() ou get_matrix().
    - Le résultat se présente sous forme d'un tableau numpy : Chaque signal de sortie occupe une colonne. 
    - Les directives de coding sont dans les docstrings de la classe-mère.


=================================================================================================================

                                                    CODE

*************** Imports ***************** """
# Imports externes
from PyQt5.QtWidgets import QWidget, QVBoxLayout, QPushButton
from PyQt5.QtCore import Qt
# ... adapter les imports externes                                                                              ☐

# Imports internes
import functions.indicators as indic
# from nodes.{groupe}.{nom}.{nom}_yaml import YamlParams      # <-- A modifier                                  ☐
from pc.ctrl_node import CtrlNode, CtrlCalcul
# ... adapter les imports internes                                                                              ☐

""" ******* Datas de reconnaissance pour l'affichage et la compilation dynamique. ******* """
d_datas = {
    'name': 'xxxxx',        # Label affiché sous l'icone.                                                       ☐
    'icon': 'xxxxx.png',    # Icone affichée.                                                                   ☐
}


class Node(CtrlNode):
    """ Déclaration de la classe Node. """
    def __init__(self, o_scene, s_id, pos):
        super().__init__(o_scene, s_id, pos)
        self.type = 'xx'  # _                                                                                   ☐
        # ... attributs spécifiques.                                                                            ☐
        self.setup()

    def setup(self, child_file=__file__):
        # self.l_short_circuits = [(0, 0)]  # Liste de court-circuits.                                          ☐
        # self.o_grcontent = UiContent(self)  # Si le node contient des widgets (boutons, ...)                  ☐
        super().setup(child_file)

#     """ ****************************** Entrées et sorties. ****************************** """
#     """ Entrées, 2 solutions : nombre fixe ou paramétrable. En choisir une, supprimer l'autre. """
#     """ Exemple de nombre d'entrées fixe (1 entrée). """
#     @property
#     def ld_inputs(self):    # Adapter ce code, ou le supprimer.                                               ☐
#         return [{
#             'label': 'xxxx',
#             'label_pos': (6, -10)
#         }]

#     """ Exemple de nombre d'entrées fixées par les paramètres. """
#     @property
#     def ld_inputs(self):    # Adapter ce code, ou le supprimer.                                               ☐
#         """ Lecture des paramètres pour connaître le nombre d'entrées. """
#         nb_inputs = self.get_param(["Nombre d'entrées"], 1)     # 1 entrée par défaut (à adapter).
#         l_inputs = list()
#         for k in range(nb_inputs):
#             """ Ajout des entrées. """
#             label = 'gr' if nb_inputs == 1 else f'gr {k}'
#             l_inputs.append({'label': label, 'label_pos': (6, -10)})
#
#             """ Ajout des séparateurs (on évite un séparateur en fin de liste). """
#             if k in self.l_sep_inputs and k < nb_inputs - 1:
#                 l_inputs.append('sep')
#
#         return l_inputs

#     """ Sorties, 2 solutions : nombre fixe ou paramétrable. En choisir une, supprimer l'autre. """
#     """ Exemple de nombre de sorties fixe (2 sorties). """
#     @property
#     def ld_outputs(self):   # Adapter ce code, ou le supprimer.                                               ☐
#         return [  # Liste de 2 dictionnaires.
#             {
#                 'label': 'xxxx',
#                 'label_pos': (-38, -10)
#             },
#             'sep',  # Séparateur (facultatif).
#             {
#                 'label': 'yyyy',
#                 'label_pos': (-59, -10)
#             }
#         ]

#     @property
#     def ld_outputs(self):   # Adapter ce code, ou le supprimer.                                               ☐
#         """ Lecture des paramètres pour connaître le nombre de sorties. """
#         nb_outputs = self.get_param(["Nombre de sorties"], 1)     # 1 sortie par défaut (à adapter).
#         l_outputs = list()
#         for k in range(nb_outputs):
#             """ Ajout des sorties. """
#             label = 's' if nb_outputs == 1 else f's {k}'
#             l_outputs.append({'label': label, 'label_pos': (-20 if nb_outputs == 1 else -30, -10)})
#
#             """ Ajout des séparateurs (on évite un séparateur en fin de liste). """
#             if k in self.l_sep_outputs and k < nb_outputs - 1:
#                 l_outputs.append('sep')
#
#         return l_outputs
#
#     """ ***************************** Paramètres statiques. *****************************
#         https://pyqtgraph.readthedocs.io/en/latest/parametertree/apiref.html """
#     def fixed0_params(self):    # Option                                                                      ☐
#         """ Ces paramètres seront affichés AVANT les couleurs. """
#         return {
#             "Nombre d'entrées": [1, {'step': 1, 'limits': (1, 8), 'compactHeight': False}],  # Entier + paramètres.
#             "Nombre de sorties": [1, {'step': 1, 'limits': (1, 8), 'compactHeight': False}],
#         }

#     def fixed_params(self):     # Option                                                                      ☐
#         """ Ces paramètres seront affichés APRÈS les couleurs. """
#         return {
#             'Type': ['Normal', {'values': ['Normal', 'Poisson', 'Binomial', 'Logistic']}],      # Liste ce choix.
#             'Émulation histos': True,                                                           # Booléen.
#             'Amplitude': [20, {'compactHeight': False}],                                        # Entier.
#             'Fenêtre': {                                                                        # Sous-dictionnaire.
#                 'Titre': 'Titre de la fenêtre',
#                 'Interface': ['Matplotlib', {'values': list(self.scripts.keys())}]
#             },
#             'Graine (<0=None)': [-1, {'tip': "Une valeur ⩾ 0 crée une série pseudo-aléatoire,"  # Message au survol.
#                                              "\nUne valeur < 0 crée une série aléatoire.", 'compactHeight': False}],
#         }

#     def bundle_params(self):    # Option (Ex : voir Plots)                                                    ☐
#         """ Ces paramètres seront affichés AVANT les paramètres dynamiques. """
#         return {
#             'Titre': 'Titre du graphique',
#         }

#     """ ************** Paramètres dynamiques appliqués aux signaux d'entrée. ************ """
#     def my_params(self, context):                                                                             ☐
#         """ Peuplement du dockable des paramètres. """
#         """ - Ce node a une ou plusieurs entrées : les blocs 'Entrée 0', 'Entrée 1', etc du dockable des paramètres.
#                 |_ Note : Les titres de ces blocs n'apparaissent que si l'entrée est connectée et active.
#             - Chacune reçoit un faisceau de signaux (0, 1 ou plusieurs signaux) provenant des nodes-serveurs.
#             - Chaque signal sera affecté d'un dictionnaire de valeurs, décrites ci-dessous.
#                 |_ Note : Ces valeurs seront utilisées pour le traitement spécifique à ce node : calculs, etc. """
#         return {
#             'Param1': 'xxx',
#             'Param2': 'xxx',
#             'etc': 'xxx'
#         }

#     """ ************************* Signaux délivrés aux sorties. ************************* """
#     def my_signals(self, l_signals_in, num_socket_out):
#         """ Décrit les signaux en sortie. Leur nombre est indépendant de celui des signaux en entrée.
#         @param l_signals_in: Liste de {nb_entrées} listes. Chacune contient 9 informations :
#             0 typ_id_from :             # Type + id du node en amont, producteur du signal. Ex : 'Aléatoire1'.
#             1 signal_ante_from:         # Nom du signal du node en amont du node from. Vide si from n'a pas d'entrées.
#             2 signal_now_from :         # Nom du signal du node from. Ex : 'Normal'
#             3 signal_source_from :      # Texte destiné à être affiché dans le dockable, dans 'Provenance du signal :'
#             4 signal_order :            # N° d'ordre du signal.
#             5 signal_title :            # Ex : 'Aléatoire1-Normal'
#             6 typ_id :                  # Ex : 'Somme3'
#             7 signal_ante :             # Ex : 'Normal'
#             8 signal_source :           # Provenance du signal, avec retours à la ligne ('\n').
#         @param num_socket_out: La valeur de retour concerne la sortie N° {num_socket_out}.
#         @return: Liste de tuples à 4 ou 5 valeurs.
#             |_ Chaque tuple décrit un signal sortant en {num_socket_out}, formaté comme ceci :
#             0 typ_id:                   # Type + id du node en cours. Ex : 'MACD3'.
#             1 signal_ante :             # Combinaison de noms des signaux entrant.
#             2 signal_now :              # Nom du signal. Ex : 'Somme'
#             3 signal_source :           # Texte affiché dans le champ 'Provenance du signal'
#             4 _legend :                 # Option : Légende dans le node suivant, si celui-ci est un afficheur. """
#
#         """      Dépendances               | signal_source |  typ_id  |  signal_ante  |  signal_now  |  legend  |
#         ---------------------------------------------------------------------------------------------------------
#         Titre affiché dans le dockable     |               |    X     |       X       |       X      |          |
#         ---------------------------------------------------------------------------------------------------------
#         Légende affichée avec option       |               |          |               |              |     X    |
#         ---------------------------------------------------------------------------------------------------------
#         Légende affichée sans option       |               |          |       X       |      X       |          |
#         ---------------------------------------------------------------------------------------------------------
#         Champ 'Provenance du signal'       |    X + \n     |    X     |               |      X       |          |
#         ---------------------------------------------------------------------------------------------------------
#         Nom utilisé dans la classe Calcul  |               |          |               |      X       |          |
#         ----------------------------------------------------------------------------------------------------- """
#
#         """ Exemple de node avec 2 sorties. """
#         l_signals = [[], []]        # Autant de sous-listes que de sorties dans ce node. Ex : ici, 2.
#         for l_sign in l_signals_in:
#             for signals_in in l_sign:
#                 """ Chaque signal d'entrée apporte ses paramètres. """
#                 typ_id_from, signal_ante_from, signal_now_from, signal_source_from, \
#                     signal_order, signal_title, typ_id, signal_ante, signal_source = signals_in[:9]
#
#                 """ Traitement. Exemples : Voir les nodes existants. """
#                 signal_now = 'xxxx'
#
#                 """ Ajoute un tuple à 4 ou 5 valeurs ('_legend' est optionnel). """
#                 l_signals[0].append((typ_id, signal_ante, signal_now, signal_source))                 # Sortie 0
#
#                 """ Traitement. Exemples : Voir les nodes existants. """
#                 signal_now = 'yyyy'
#                 _legend = 'zzzz'       # Option.
#
#                 """ Ajoute un tuple à 4 ou 5 valeurs ('_legend' est optionnel). """
#                 l_signals[1].append((typ_id, signal_ante, signal_now, signal_source, _legend))        # Sortie 1
#
#         """ Faisceau de signaux (l_signals) délivré à la sortie. """
#         return l_signals

#     """ **************** Un paramètre du dockable vient d'âtre modifié. ***************** """
#     def refresh(self, l_keys):
#         """ Retirer ces 4 lignes si le node n'a pas un nombre d'entrées/sorties paramétrable. """
#         if l_keys[-1].startswith("Nombre d"):
#             self.rebuild(b_input=l_keys[-1].endswith("entrées"))
#             """ Le socket ajouté ou retiré peut modifier l'infrastructure. """
#             l_keys = ['infrastructure'] + l_keys
#
#         # ... Code spécifique ...
#
#         super().refresh(l_keys)


# class UiContent(QWidget):  # Option : Seulement si le node affiche un bouton ou autre widget.                 ☐
#     """ Widgets affichés dans le node (ici, un bouton). L'affichage est assuré par 'UiNode.initContent()'. """
#     def __init__(self, o_parent):
#         super().__init__()
#         self.o_parent = o_parent
#
#         """ Le style est dans qss """
#         self.setGeometry(40, 38, 10, 10)
#         self.layout = QVBoxLayout()
#         self.layout.setContentsMargins(0, 0, 0, 0)  # g, h, d, b
#         self.setLayout(self.layout)
#
#         """ Bouton : le style est dans qss/nodestyle.qss. """
#         self.button = QPushButton('Voir', self)
#         self.button.setCursor(Qt.PointingHandCursor)
#         self.layout.addWidget(self.button)
#         self.button.clicked.connect(self.o_parent.show_figure)


# class Calcul(CtrlCalcul):
#     """ ******** Le code ci-dessous ne concerne pas le poste de contrôle, mais seulement les calculs. ******** """
#     def __init__(self):
#         super().__init__()
#         self.o_yaml = YamlParams(self)
#         # Attributs spécifiques ...
#
#     def descr_signal(self, od_descr, val, root_key):
#         """ - Voir docstring dans la classe-mère.
#             - Préparation du super-dictionnaire 'od_descr' nécessaire à process() et/ou à get_matrix(). """
#         pass
#
#     def process(self):
#         """ - Voir docstring dans la classe-mère.
#             - Méthode full, utilisée en mode non-générateur-python.
#             - A l'issue du traitement, le tableau numpy 'self.np_array' est entièrement rempli.
#             - En mode générateur-python, on peut toutefois utiliser cette méthode pour faire un pré-traitement.
#                 - Dans ce cas, décommenter le code après le pass.
#         """
#         pass
#         # Pré-traitement en mode générateur-python ...
#         # ... à coder.
#
#     """ Commenter ou supprimer cette méthode en mode non-générateur-python. """
#     def get_matrix(self, pointer, nb_lines=0):
#         """ - Voir docstring dans la classe-mère.
#             - Traitement ponctuel et partiel, utilisé en mode générateur-python.
#             - A l'issue du traitement, une toute petite partie de tableau numpy 'self.np_array' a été remplie. """
#         return super().get_matrix(pointer, nb_lines)

 


Traitement d'un exemple complet : un additionneur.

Le node additionneur et ses paramètres.

Création d'une sinusoïde bruitée.


Plan du tuto :
  1. Préparation.
    1. Description.
    2. Les fichiers de paramètres avancés.
    3. Adaptation du code de base.
  2. Node dans le poste de contrôle (PC).
    1. Dockable des nodes.
    2. Affichage dans la scène.
    3. Les entrées-sorties.
    4. Paramètres fixes.
    5. Signaux reçus aux entrées.
    6. Signaux délivrés aux sorties.
    7. Widgets dans le node.
  3. Calculs.
    1. Propagations.
    2. Signature d'un node.
    3. Persistance des résultats.
    4. Éléments de calculs.
    5. Création du dictionnaire self.od_descr.
    6. Coding par itérations successives.
    7. Les méthodes descr_signal() et process().

 


☐ 1.1 - Préparation / Description :

Retour au plan


1.2 - Préparation / Les fichiers de paramètres avancés (yaml) :

Retour au plan


1.3 Adaptation du code de base :

Retour au plan

 


2.1 - PC / Dockable des nodes :

Retour au plan


2.2 - PC / Affichage de ce nouveau node dans la scène

Retour au plan


2.3 - PC / Les entrées-sorties :

Retour au plan


2.4 - PC / Paramètres fixes

Retour au plan


2.5 - PC / Signaux reçus aux entrées :

Retour au plan


2.6 - PC / Signaux délivrés aux sorties :

Retour au plan

Correspondance entre les variables de la méthode my_signals() et les paramètres affichés des nodes-clients (ici, Plots).
On voit que l'unique signal de sortie de ADD-0 a pour nom : ADD0-Somme.

 


2.7 - PC / Widgets dans le node :

Retour au plan


3 - Calculs

Retour au plan

3.1 - Calculs / Propagations :

Retour au plan


3.2 - Calculs / Signature :

Retour au plan

Ces explications sont ici à titre informatif :
    - Tout le mécanisme est déjà codé dans la classe-mère CtrlCalcul.
    - Nous n'avons pas à nous en occuper au niveau des classes dérivées.


3.3 - Calculs / Persistance :

Retour au plan


3.4 - Calculs / Éléments de calculs :

Retour au plan


3.5 - Calculs / Création du dictionnaire self.od_descr :

Retour au plan

Aide à la programmation : afficher ces super-dictionnaires via leur méthode print().
    - Exemple : self.od_mydic.print().

A droite : le dictionnaire que l'on désire obtenir.
TRÈS IMPORTANT ! Le nom du signal doit être ADD0-Somme (Voir image précédente).


3.6 - Calculs / Coding par itérations :

Retour au plan


3.7 - Calculs / Les méthodes descr_signal() et process() :

Retour au plan


Vérification

  • ☐ Modifier si nécessaire les paramètres avancés (yaml) du node Plots pour obtenir ce résultat :

Notez la valeur moyenne à 10, qui correspond à l'offset (ou biais).
 

  • ☐ Tester d'autres possibilités : offset, plus de 2 signaux, paramètres différents : amplitudes, fréquence, etc.
  • ☐ Nettoyer le code, refactorer si nécessaire.

Bonjour les codeurs !