C'est la finalité du poste de contrôle.
Avant-propos
socket_in sont concernés par ce bug.UiSocket.UiSocket.state_brush() :
@property
def state_brush(self):
""" Couleur de fond : paramétrée (on), gris (off), ou blanc (survol). """
color_off = self.o_socket.o_node.d_display['col_brush']['off']
if self.o_socket.b_input:
o_edge = list(self.o_socket.get_gredges())[0].o_edge if len(self.o_socket.get_gredges()) == 1 else None
color = o_edge.color if o_edge is not None and o_edge.b_on else color_off
else:
color = self.o_socket.color if self.o_socket.o_node.b_on else color_off
color = '#fff' if self.b_hover else color
return pg.mkBrush(color)
CtrlEdge.setup(), supprimer la ligne self.o_socket_in.color = self.color
Description
La finalité du poste de contrôle est la production de fichiers calcul.pkl, qui contiennent les modèles de calculs, sous forme d'un super-dictionnaire OrderedDict.
calcul.pkl pour chaque graphique à afficher.Plots représente un graphique.Plots.
Plots peut avoir plusieurs graphiques, qui seront affichés dans une même fenêtre.
calcul.pkl.

Sans voir le reste du graphe, nous constatons que ce node possèdera 3 fichiers calcul.pkl.
socket in de Plots) est produit par certains nodes (pas tous !)

Exemple de graphe.
Plots) sont influencés par les nodes en amont :calcul.pkl vont être créés : 2 pour le node 9 et 5 pour le node 10.
node_9/calc_0.pkl, node_9/calc_1.pklnode_10/calc_0.pklnode_10/calc_1.pklnode_10/calc_2.pklnode_10/calc_3.pklnode_10/calc_4.pklPlots, les sorties sont des graphiques.pkl si d'autres process Python doivent s'en servir.CtrlScene.recalculate() doit être appelée :
recalculate() à la fin de cette méthode infrastructure().CtrlNode.refresh() est appelée.
recalculate() à la fin de cette méthode refresh().recalculate() délègue les calculs aux sockets in des nodes terminaux → CtrlSocket.recalculate().Plots) → plots.py > Node.recalculate().Il y a donc 2 méthodes à modifier et 3 méthodes à créer :
CtrlScene.infrastructure() ← à modifier.CtrlNode.refresh() ← à modifierCtrScene.recalculate()CtrlSocket.recalculate()plots.py > Node.recalculate()plots.py > Node.recalculate() possède un paramètre, num_input.
Plots, mais vous pourrez en ajouter selon vos besoins.recalculate() des nodes finaux lance la propagation vers l'amont :
d_calc.
d_calc est une navette, qui s'incrémente au fur et à mesure, par modification 'en place'.d_calc à son socket d'entrée N° num_input : CtrlSocket.update_dcalc(d_calc).d_calc à son edge : CtrlEdge.update_dcalc(d_calc).socket out la même chose : CtrlSocket.update_dcalc(d_calc).socket out demande aussi à son node de faire la mise à jour de son dictionnaire : CtrlNode.update_dcalc(d_calc).node demande la même chose à chacun de ses sockets d'entrée, dans une boucle : CtrlSocket.update_dcalc(d_calc).
d_calc.d_calc avec ses propres paramètres d'affichage (légende, couleur, type de trait, ...)pkl seulement en cas de modifications.Il y a donc 3 méthodes à créer :
CtrlSocket.update_dcalc()CtrlEdge.update_dcalc()CtrlNode.update_dcalc()Code :
CtrlScene.infrastructure() (Dernière ligne ajoutée) :
def infrastructure(self):
""" Le but de cette méthode est de :
- Positionner le flag b_on de chaque node.
- Positionner le flag b_on de chaque edge.
- Rafraîchir l'affichage pour griser les éléments inactifs (b_on == False).
"""
l_status_ante = list()
l_grnodes, l_gredges = self.get_grnodes(), self.get_gredges() # Capture unique pour ne pas alourdir.
""" On positionne le flag de chaque node, selon son état initial. """
for o_grnode in l_grnodes:
o_grnode.o_node.b_on = o_grnode.o_node.get_state()
l_status_ante.append(o_grnode.o_node.b_on)
""" On lève tous les flags des edges. """
for o_gredge in l_gredges:
o_gredge.o_edge.b_on = True
l_status_ante.append(True)
for nb_loops in range(10):
""" Positionnement des flags b_on de tous les nodes et de tous les edges. """
""" État de chaque edge = 'OFF' si l'état de son node de départ OU d'arrivée est 'OFF'. """
for o_grnode in l_grnodes:
if not o_grnode.o_node.b_on:
""" Si le node est 'OFF', on baisse le drapeau de chacun de ses edges. """
for o_socket in o_grnode.o_node.lo_sockets_in + o_grnode.o_node.lo_sockets_out:
for o_gredge in list(o_socket.get_gredges()):
o_gredge.o_edge.b_on = False
""" État d'un node = 'OFF' si tous ses edges d'entrée OU de sortie sont 'OFF' ou absents. """
l_status = list()
for o_grnode in l_grnodes:
if o_grnode.o_node.b_on:
b_in, b_out = len(o_grnode.o_node.lo_sockets_in) == 0, len(o_grnode.o_node.lo_sockets_out) == 0
for o_socket in o_grnode.o_node.lo_sockets_in:
for o_gredge in list(o_socket.get_gredges()):
if o_gredge.o_edge.b_on:
b_in = True
for o_socket in o_grnode.o_node.lo_sockets_out:
for o_gredge in list(o_socket.get_gredges()):
if o_gredge.o_edge.b_on:
b_out = True
if not (b_in and b_out):
o_grnode.o_node.b_on = False
""" Status du graphe. """
l_status.append(o_grnode.o_node.b_on)
for o_gredge in l_gredges:
l_status.append(o_gredge.o_edge.b_on)
if l_status == l_status_ante:
""" Si aucun changement, terminé ! """
break
""" Mémorisation du dernier état (copie profonde - on utilise CtrlNode.deepcopy()). """
l_status_ante = l_grnodes[0].o_node.deepcopy(l_status)
""" Rafraîchit l'affichage du graphe. """
self.o_grscene.update()
""" Recalculs au niveau de chaque socket. """
self.recalculate()
A la fin du code, les calculs sont mis à jour.
CtrlNode.refresh() (Dernière ligne ajoutée) :
def refresh(self, l_keys):
if l_keys[-1] == 'Titre du node':
""" Titre du node. """
self.o_grnode.set_title()
elif l_keys[-2] == 'Couleurs':
""" Couleur des sockets et des edges. """
self.set_colors(l_keys[1:])
else:
""" Fin de refresh - Appel conditionnel à infrastructure(). """
if self.b_chk and (l_keys[0] == 'infrastructure' or l_keys[-1] == 'Signal actif'):
""" Les nodes de type générateur doivent insérer le mot 'infrastructure'
en 1ère place dans la liste l_keys[], car ils n'ont pas 'Signal actif' dans leurs paramètres. """
self.o_scene.infrastructure()
""" Tous les paramètres, autres que le titre et les couleurs, influencent les calculs. """
self.o_scene.recalculate()
CtrlScene.recalculate() :
def recalculate(self):
""" Outre les calculs (point 2), cette méthode fait aussi du nettoyage (points 1 et 3). """
""" 1 - Suppression des dossiers devenus inutiles (ils ont existé, puis ont été supprimés du graphe). """
""" Liste des dossiers physiquement présents dans le disque dur. """
bk_path = os.path.abspath(os.path.dirname(__file__).replace('\\pc', '\\backups') + '/' + self.graph_name)
l_folders = next(os.walk(bk_path))[1]
""" Liste des nodes dans le graphe. """
l_nodes = [o_grnode.o_node.s_id.lower() for o_grnode in self.get_grnodes()]
for folder in l_folders:
f = folder.lower()
if f.startswith('node') and f not in l_nodes:
""" shutil.rmtree() supprime le dossier, même non-vide, contrairement à os.rmdir(). """
shutil.rmtree(bk_path + os.sep + f)
""" 2 - Tous les calculs sont faits vers l'amont, en partant des nodes finaux (ex: Plots). """
l_ends = list()
for o_grnode in self.get_grnodes():
if not o_grnode.o_node.lo_sockets_out: # Pas de socket out => Node final.
l_ends.append(o_grnode.o_node.id)
""" Recalculs au niveau de chaque socket. """
for o_socket_in in o_grnode.o_node.lo_sockets_in:
o_socket_in.recalculate()
""" 3 - Suppression des fichiers de calcul exédentaires, donc inutiles. """
l_edges = [edge[1] for edge in self.o_pkl.od_pkl.read('edges') if edge[1][0] in l_ends]
l_folders = next(os.walk(bk_path))[1] # Utiliser print() pour comprendre ces 2 lignes.
for folder in l_folders:
id_node = int(folder[4:]) # node16 => 16
l_calc_files = next(os.walk(os.path.join(bk_path, folder)))[2]
for cal_file in l_calc_files:
if cal_file.startswith('calc_'):
id_socket = int(os.path.splitext(cal_file)[0].split('_')[1]) # calc_3.pkl => 3
if (id_node, id_socket) not in l_edges:
surplus_file = os.path.join(bk_path, folder, cal_file)
if os.path.isfile(surplus_file):
os.remove(surplus_file)
Importer os et shutil.
CtrlSocket.recalculate() :
def recalculate(self):
self.o_node.recalculate(self.t_id[1])
plots.py > Node.recalculate() :
def recalculate(self, num_input):
d_calc = {'edges': set()}
o_socket_in = self.lo_sockets_in[num_input]
o_socket_in.update_dcalc(d_calc)
od_params = self.new_od(self.o_params.od_params[self.main_key])
od_calc = self.new_od()
od_calc.write('Compute', self.type)
for key in od_params.key_list():
if key[0] == 'Titre du node' or key[0] == 'Couleurs':
continue
if key[-1] == '$Provenance du signal :' or key[-1] == "Nombre d'entrées":
continue
b_add = True
if key[0].startswith('Entrée'):
t_socket = self.id, int(key[0][7:])
b_add = False
for edge in d_calc['edges']:
if t_socket in edge:
b_add = True
break
if b_add:
od_calc.write(key, od_params.read(key))
d_calc[self.s_id] = dict(od_calc)
""" On n'enregistre le pkl que s'il a été modifié. """
if d_calc != self.ld_calc[num_input]:
fic_pkl = os.path.abspath(f'{self.path_node}/calc_{num_input}.pkl')
try:
with open(fic_pkl, 'wb') as pk:
pickle.dump(d_calc, pk, pickle.HIGHEST_PROTOCOL)
""" MAP à supprimer. """
if len(self.lo_sockets_in[num_input].get_gredges()) > 0:
print(f"Fichier 'node{self.id}/calc_{num_input}.pkl' modifié.")
""" MAP à supprimer. """
self.ld_calc[num_input] = self.deepcopy(d_calc) # Mémorisation.
except (Exception,):
pass
Importer pickle et os.
new_od() n'existe pas. Créons-la.CtlNode.new_od() :
@staticmethod
def new_od(dic=None):
return Dictionary(dic)
self.ld_calc et self.path_node n'existent pas.
init.self.setup() dans plots.py > Node.__init__() :
self.path_node = ''
self.ld_calc = [None for i in range(8)] # Liste de 8 None.
plots.py > Node.setup() :
self.init_calc()
plots.py > Node.init_calc() :
def init_calc(self):
""" Persistance dans un pkl. Un sous-dossier par node. """
self.path_node = os.path.abspath(
f"{os.path.dirname(__file__).split('nodes')[0]}backups/{self.o_scene.graph_name}/node{self.id}")
os.makedirs(self.path_node, exist_ok=True) # Création dossier. Si ce dossier existe, pas d'erreur renvoyée.
""" Affectaion de self.ld_calc[] : Lecture des modèles de calcul mémorisés. """
for num_input in range(len(self.lo_sockets_in)):
fic_pkl = os.path.abspath(f'{self.path_node}/calc_{num_input}.pkl')
if os.path.isfile(fic_pkl):
try:
""" Lecture du fichier pkl. """
with open(fic_pkl, 'rb') as pk: # Pas d'encoding pour les fichiers binaires.
self.ld_calc[num_input] = pickle.load(pk)
except (Exception,):
pass
CtrlSocket.update_dcalc() :
def update_dcalc(self, d_calc):
""" Propagation vers l'amont. """
if self.b_input:
l_edges = list(self.get_gredges())
if l_edges:
""" Il ne peut y avoir qu'un seul edge dans la liste. """
o_edge = list(self.get_gredges())[0].o_edge # edge unique à l'indice 0.
d_calc['edges'].add(o_edge.get_tid())
o_edge.update_dcalc(d_calc)
else:
self.o_node.update_dcalc(d_calc)
CtrlEdge.update_dcalc() :
def update_dcalc(self, d_calc):
""" Propagation vers l'amont. Aucun calcul. Simple relai. """
self.o_socket_out.update_dcalc(d_calc)
CtrlNode.update_dcalc() :
def update_dcalc(self, d_calc):
if not self.b_on:
""" Stop propagation. """
return
""" Propagation amont. """
for o_socket_in in self.lo_sockets_in:
o_socket_in.update_dcalc(d_calc)
od_params = Dictionary(self.o_params.od_params[self.main_key])
""" Construction du dictionnaire pour les calculs, à partir du od_params. """
od_calc = Dictionary()
od_calc.write(['Compute'], self.type)
for key in od_params.key_list():
if key[0] == 'Titre du node' or key[0] == 'Couleurs':
continue
if key[-1] == '$Provenance du signal :':
continue
od_calc.write(key, od_params.read(key))
""" Modification 'en place'. """
d_calc[self.s_id] = dict(od_calc)
Plots, il apparaît une erreur au delà de la 3ème entrée.rebuild(), dans la partie """ Régénération des sockets """.
i pour la position du socket et pour son numéro, or ce ne sont pas les mêmes.
plots.py > Node.rebuild() :
def rebuild(self):
""" Reconstruction du node suite à un changement dynamique du nombre d'entrées. """
""" Suppression des (sockets + edges) de ce node. """
for o_socket in self.lo_sockets_in:
lo_gredges = list(o_socket.get_gredges())
if lo_gredges:
""" Suppression des éventuels edges connectés à ce socket. """
self.o_scene.o_grscene.removeItem(lo_gredges[0])
""" Suppression du socket. """
self.o_scene.o_grscene.removeItem(o_socket.o_grsocket)
o_socket.o_grsocket = None # Suppression de l'objet.
""" Nettoyage de la liste des entrées. """
self.lo_sockets_in.clear()
""" Redessine le node avec sa nouvelle hauteur. """
ld_inputs = self.deepcopy(self.ld_inputs)
nb_inputs = len(ld_inputs)
h_title = self.d_display['geometry']['h_title']
first_y = 16 * (1 + (h_title + 4) // 16)
self.height = first_y + nb_inputs * 16 - 4 # Nouvelle hauteur.
self.o_grnode.set_outline()
""" Régénération de sockets. """
for pos, d_input in enumerate(ld_inputs):
""" On complète les dictionnaires de base. Pas de séparateurs. """
if isinstance(d_input, dict):
indx = len(self.lo_sockets_in)
d_input['pos'] = True, pos # On ajoute la pos. Tuple (input: True, num_position).
d_input['id'] = self.id, indx # On ajoute l'identifiant. Tuple (id_node, num_socket)
self.lo_sockets_in.append(CtrlSocket(self, d_input)) # Socket instancié et ajouté à la liste.
""" Régénération des edges. """
self.o_scene.show_edges()
self.o_scene.save_edges()
""" Dockable des paramètres : le nombre d'entrées a changé. """
self.o_params.set_params()
self.dt.delay(self.o_scene.show_params, 2000) # delay : Permet de continuer la saisie dans le dockable.
CtrlEdge, lorsqu'on modifie le nombre d'entrées d'un Plots, puis le nombre d'entrées d'un autre Plots.
UiSocket fantômes (présents en mémoire mais absents physiquement - NoneType).try / except lorsque cette erreur se produit.@property CtrlEdge.end_points() :
@property
def end_points(self):
""" - Renvoie un tuple : les points de départ et d'arrivée.
- Les coordonnées d'un socket sont relatives à celles de son node.
- Il faut donc les additionner pour obtenir les coordonnées absolues."""
""" Point de départ. """
pos_node_from = self.o_socket_out.o_node.o_grnode.pos() # type QPointF
pos_socket_out = self.o_socket_out.o_grsocket.pos() # type QPointF
""" Point d'arrivée. """
try:
if self.o_socket_in.__class__.__name__ == 'QPointF':
""" Edge en construction : pos_to = position de la souris. """
pos_to = self.o_socket_in # type QPointF
else:
pos_node_to = self.o_socket_in.o_node.o_grnode.pos() # type QPointF
pos_socket_in = self.o_socket_in.o_grsocket.pos() # type QPointF
pos_to = pos_node_to + pos_socket_in
""" Les QPointF peuvent s'additionner. """
return pos_node_from + pos_socket_out, pos_to # Tuple.
except (Exception,):
return QPointF(1e5, 1e5), QPointF(1e5, 1e5) # Si erreur, 2 points confondus, hors écran.
Importer QPointF
Vérification

Pour la vérification, tous les types de nodes sont présents.
plots.py > Node.recalculate() contient le code de vérification suivant :
""" MAP à supprimer. """
if len(self.lo_sockets_in[num_input].get_gredges()) > 0:
print(f"Fichier 'node{self.id}/calc_{num_input}.pkl' modifié.")
""" MAP à supprimer. """
Vérifier les messages affichés suite aux modifications suivantes :
Bonjour les codeurs !