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.pkl
node_10/calc_0.pkl
node_10/calc_1.pkl
node_10/calc_2.pkl
node_10/calc_3.pkl
node_10/calc_4.pkl
Plots
, 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 !