import de fichiers

This commit is contained in:
2024-02-28 11:40:09 +01:00
parent 25610fd4fa
commit 88aced27bf
112 changed files with 3678 additions and 0 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

52
modele/cocosim.py Normal file
View File

@@ -0,0 +1,52 @@
from modele.matrice import Matrice
class Cocosim(object):
def __init__(self, conf, graphe, scenario, observateur):
self.conf = conf
self.graphe = graphe
self.scenario = scenario
if 'max' in self.scenario:
self.max_cycles = self.scenario['max']
else:
self.max_cycles = None
self.n_cycle = 0
self.matrice = Matrice(self.graphe)
self.observatoire = {}
self.observateur = observateur
for k,_,_ in self.observateur:
self.observatoire[k] = []
def cycle(self):
"""
Un cycle se fait en deux passes plus la mise à jour des composants.
"""
if self.n_cycle in self.scenario:
[f(self.graphe[x]) for x, f in self.scenario[self.n_cycle]]
self.graphe.init_cycle()
# Passe 1
self.matrice.init_mat()
xvect = self.matrice.solve()
self.graphe.passe_calcul(xvect)
# Passe 2 (pour les diodes)
self.matrice.init_mat()
xvect = self.matrice.solve()
self.graphe.passe_calcul(xvect)
# Mise à jour des états
self.graphe.update()
self.graphe.coherence()
for k, obj, fun in self.observateur:
self.observatoire[k].append(fun(self.graphe[obj]))
self.n_cycle += 1
def run(self):
for cycle in range(self.max_cycles):
self.cycle()

264
modele/composants.py Normal file
View File

@@ -0,0 +1,264 @@
from modele.exceptions import NonPolariseException, Dir
class Composant(object):
def __init__(self, conf, nom):
"""
Un composant doit a minima savoir donner une résistance
"""
self.branche = None # Pour récupérer l'intensité dans la branche
self.nom = nom
self.conf = conf
self.r = None
def init_cycle(self):
"""
Initialisation du composant avant la 1ere passe de calcul.
Pour la plupart des composants (ceux qui se calculent en une seule passe) on ne fait
rien. N'est utile que pour les composants se calculant en plusieurs passes (diodes...).
"""
pass
def passe_calcul(self):
"""
Initialisation du composant apres la 1ere passe de calcul.
Pour la plupart des composants (ceux qui se calculent en une seule passe) on ne fait
rien. N'est utile que pour les composants se calculant en plusieurs passes (diodes...). """
pass
def update(self):
# Par défaut ne fait rien
pass
def reverse(self):
"""
Permet de retourner les composants polarisés (s'ils sont préfixés par "!")
"""
raise NonPolariseException
def etat(self):
raise NotImplementedError
class Contact(Composant):
def __init__(self, conf, nom, contact_type):
super().__init__(conf, nom)
self.ouvert = None
self.ouvre()
self.type = contact_type
def ouvre(self):
self.ouvert = True
self.r = self.conf['RMAX']
def ferme(self):
self.ouvert = False
self.r = self.conf['RMIN']
def etat(self):
return not self.ouvert
def bascule(self):
if self.ouvert:
self.ferme()
else:
self.ouvre()
class Resistance(Composant):
def __init__(self, conf, nom, valeur):
super().__init__(conf, nom)
self.RDEFAULT = self.conf["RES_DEFAULT"]
if valeur is None:
self.r = self.RDEFAULT
else:
self.r = valeur
def set_r(self, valeur):
self.r = valeur
def etat(self):
return None
class Lampe(Resistance):
def __init__(self, conf, nom):
self.RLAMPE = conf["RES_LAMPE"]
self.SEUIL = conf["SEUIL_LAMPE"]
super().__init__(conf, nom, self.RLAMPE)
self.haut = False
self.chute()
def monte(self):
self.haut = True
def chute(self):
self.haut = False
def update(self):
super().update()
if abs(self.branche.i()) >= self.SEUIL:
self.monte()
else:
self.chute()
def etat(self):
return self.haut
class Relais(Composant):
def __init__(self, conf, nom, graphe):
super().__init__(conf, nom)
self.graphe = graphe
self.r = self.conf["RES_RELAIS"]
self.SEUIL = self.conf["SEUIL_RELAIS"]
prefix = nom.split('.')
self._contacts_travail = []
self._contacts_repos = []
self._contacts_double = []
self.haut = False
def add_travail(self, nom):
self._contacts_travail.append(nom)
def add_repos(self, nom):
self._contacts_repos.append(nom)
def add_double(self, nom):
self._contacts_double.append(nom)
def monte(self):
self.haut = True
for nom in self._contacts_repos:
self.graphe[nom].ouvre()
for nom in self._contacts_travail:
self.graphe[nom].ferme()
for nom in self._contacts_double:
self.graphe[nom].active()
def chute(self):
self.haut = False
for nom in self._contacts_repos:
self.graphe[nom].ferme()
for nom in self._contacts_travail:
self.graphe[nom].ouvre()
for nom in self._contacts_double:
self.graphe[nom].desactive()
def update(self):
super().update()
if self.branche.i() >= self.SEUIL:
self.monte()
else:
self.chute()
def etat(self):
return self.haut
class DemiRelaisBasculeur(Composant):
def __init__(self, conf, nom, graphe, cote):
super().__init__(conf, nom)
self.SEUIL = self.conf["SEUIL_RELAIS"]
self.r = self.conf["RES_RELAIS"]
self._contacts_basc = []
self.haut = False
self.cote = cote
self.graphe = graphe
def add_contact(self, nom):
self._contacts_basc.append(nom)
def monte(self):
self.haut = True
if self.cote == Dir.GAUCHE:
for nom in self._contacts_basc:
self.graphe.elements[nom].bascule_gauche()
elif self.cote == Dir.DROITE:
for nom in self._contacts_basc:
self.graphe.elements[nom].bascule_droite()
else:
assert False
def chute(self):
self.haut = False
def update(self):
super().update()
if self.branche.i() >= self.SEUIL:
self.monte()
else:
self.chute()
def etat(self):
return self.haut
class Tempo(Composant):
"""
Tempo type "bilame piloté par le temps"
"""
def __init__(self, conf, nom, tempo_blocage, tempo_liberation):
super().__init__(conf, nom)
self.SEUIL = self.conf["SEUIL_TEMPO"]
self.h_trans = 0 # heure de la transition
self.h = 0 # heure courante
self.i_prec = 0.0 # intensité précédente
self.tempo_blocage = tempo_blocage / self.conf["DT"] # durée de la tempo de blocage
self.tempo_liberation = tempo_liberation / self.conf["DT"] # durée de la tempo de blocage
self.blocage = False
self.r = self.conf['RMIN']
def update(self):
if self.blocage:
if self.h - self.h_trans >= self.tempo_liberation:
self.h_trans = self.h
self.blocage = False
self.r = self.conf["RMIN"]
else:
if self.branche.i() >= self.SEUIL:
if self.i_prec < self.SEUIL:
self.h_trans = self.h # On mémorise l'heure de la transition
if self.h - self.h_trans >= self.tempo_blocage - 1:
self.r = self.conf["RMAX"]
self.blocage = True
self.h_trans = self.h # On mémorise l'heure à laquelle on bloque
else:
pass # On est au-delà du seuil mais la tempo n'est pas échue
self.i_prec = self.branche.i()
self.h += 1
@property
def ouvert(self):
return self.blocage
def etat(self):
return not self.blocage
class Diode(Composant):
def __init__(self, conf, nom):
super().__init__(conf, nom)
self.r = self.conf["RMAX"]
self.reversed = False # La diode est dans le sens opposé à la branche
def init_cycle(self):
self.r = self.conf["RMAX"]
def passe_calcul(self):
if (not self.reversed and (self.branche.amont.u() > self.branche.aval.u())) \
or (self.reversed and (self.branche.amont.u() < self.branche.aval.u())):
self.r = self.conf["RMIN"]
else:
self.r = self.conf["RMAX"]
def reverse(self):
self.reversed = True
def etat(self):
return None

28
modele/config.json Normal file
View File

@@ -0,0 +1,28 @@
{
"circuit": {
"DT": 0.05,
"RMIN": 1E-32,
"RMAX": 1E32,
"UMAX": 24.5,
"USEUIL": 5,
"ISEUIL": 0.05,
"IMAX": 5.0,
"RES_DEFAULT": 100,
"RES_LAMPE": 100.0,
"SEUIL_LAMPE": 0.05,
"RES_RELAIS": 100.0,
"SEUIL_RELAIS": 0.05,
"SEUIL_TEMPO": 0.05,
"PULSE_PERIODE": 1.0
},
"ui": {
"DT": 0.13,
"BLOCK_H": 50,
"BLOCK_W": 40,
"RELAISBASC_WIDTH": 1.6,
"LINE_W": 1,
"WINDOW_W": 1100,
"WINDOW_H": 600
}
}

356
modele/donnees.py Normal file
View File

@@ -0,0 +1,356 @@
import logging
class GraphStructException(Exception):
pass
class Noeud(object):
def __init__(self, donnees):
self.nom = donnees['nom']
self.donnees = donnees
self.type = donnees['type']
self.assoc_connecteurs = {}
def __str__(self):
return f"<Noeud : {self.nom}, {self.type}>"
def set_branche(self, direction, branche):
# Associe la branche correspondant à un connecteur
if direction in self.assoc_connecteurs:
self.assoc_connecteurs[direction].append(branche)
else:
self.assoc_connecteurs[direction] = [branche]
def get_branches(self, direction):
return self.assoc_connecteurs[direction]
def get_branche(self, direction):
c = self.get_branches(direction)
if len(c) == 1:
return c[0]
else:
raise GraphStructException("Invalid number of connectors")
def remove_branche(self, branche, direction):
self.assoc_connecteurs[direction].remove(branche)
class Arete(object):
def __init__(self, amont, amont_dir, aval, aval_dir):
self.amont = amont
self.amont_dir = amont_dir
self.aval = aval
self.aval_dir = aval_dir
def deconnecte(self):
self.amont.remove_branche(self, self.amont_dir)
self.aval.remove_branche(self, self.aval_dir)
self.amont = None
self.aval = None
self.amont_dir = None
self.aval_dir = None
def reconnecte_noeud(self, nsrc, ndir_src, ndest, ndir_dest):
"""
Déconnecte le noeud source, et le remplace par le noeud dest
"""
if nsrc == self.amont and self.amont_dir == ndir_src:
self.amont.remove_branche(self, self.amont_dir)
self.amont = ndest
self.amont_dir = ndir_dest
ndest.set_branche(ndir_dest,self)
elif nsrc == self.aval and self.aval_dir == ndir_src:
self.aval.remove_branche(self, self.aval_dir)
self.aval = ndest
self.aval_dir = ndir_dest
ndest.set_branche(ndir_dest,self)
else:
raise GraphStructException
def get_neighbour(self, noeud, direction):
if self.amont == noeud and self.amont_dir == direction:
return self.aval, self.aval_dir
elif self.aval == noeud and self.aval_dir == direction:
return self.amont, self.amont_dir
else:
raise GraphStructException("No such neighbour")
def get_amont(self):
return self.amont, self.amont_dir
def get_aval (self):
return self.aval, self.aval_dir
class Branche(Arete):
def __init__(self, amont, amont_dir, aval, aval_dir, composants=None):
super().__init__(amont, amont_dir, aval, aval_dir)
self.composants = [] if composants is None else composants
class Condensateur(object):
"""
Un condensateur correspond en fait à l'objet connecteur câblé à deux noeuds amonts et aval
C'est donc "à la fois un noeud et deux branches"
"""
def __init__(self, donnees):
self.nom = donnees['nom']
self.donnees = donnees
self.type = donnees['type']
self.amont = None
self.aval = None
self.amont_dir = None # le connecteur du noeud amont auquel on est raccordé
self.aval_dir = None # le connecteur du noeud aval auquel on est raccordé
def get_amont(self):
return self.amont, self.amont_dir
def get_aval (self):
return self.aval, self.aval_dir
class SchemaReader(object):
"""
A la lecture du graphe, tous les objets sont des noeuds et ils sont raccordés par des branches.
La simplification permet de supprimer les coudes.
La réduction des branches permet de transformer une série de branches et de composants en une seule branche
contentant les composants.
"""
REDUCTIBLE = ["ContactRepos", "Relais", "Tempo", "ContactTravail", "Bouton", "Resistance",
"Diode", "Lampe", "RelaisCote", "Levier"]
RIEN_A_FAIRE = ["P24", "P0", "Pulse", "Noeud", "Condensateur", "ContactBasculeur", "ContactDouble"]
ID_NOM = -1
def __init__(self, d):
self.noeuds = {}
self.condensateurs = {}
self.branches = []
self.composants = None
self.lire_donnees(d)
@classmethod
def nouveau_nom(cls, prefix=""):
"""
Crée un nouveau nom
"""
cls.ID_NOM += 1
return f"@{prefix}{cls.ID_NOM}"
def connecte(self, n1, d1, n2, d2, composants=None):
"""
Crée une branche et connecte deux noeuds avec
"""
a = Branche(n1, d1, n2, d2, composants=composants)
self.branches.append(a)
n1.set_branche(d1, a)
n2.set_branche(d2, a)
def connecte_condensateur_amont(self, cond, n1, d1):
cond.amont = n1
cond.amont_dir = d1
def connecte_condensateur_aval(self, cond, n1, d1):
cond.aval = n1
cond.aval_dir = d1
def remove_branche(self, branche):
self.branches.remove(branche)
branche.deconnecte()
return branche.composants
def remove_noeud(self, noeud):
del self.noeuds[noeud]
def lire_donnees(self, d):
"""
Lit le dictionnaire contenant le schéma
"""
# On commence par lire les noeuds
try:
for nom, donnees in d['blocs'].items():
donnees['nom'] = nom
self.noeuds[nom] = Noeud(donnees)
# On lit (et lie) ensuite les câbles
for amont, dir_amont, aval, dir_aval in d['cables']:
self.connecte(self.noeuds[amont], dir_amont, self.noeuds[aval], dir_aval)
except KeyError:
raise GraphStructException
def traiter_composites(self):
"""
Traite les noeuds composites (contacts doubles, relais basculeurs, etc.) et les transforme en objets simples.
"""
noeuds_a_supprimer = []
noeuds_a_ajouter = []
for nom, noeud in self.noeuds.items():
# Parcoure les noeuds en cherchant les noeuds composites
if noeud.type == "RelaisCote":
# On transforme en relais simple
noeud.type = "Relais"
if 'orientation' not in noeud.donnees or noeud.donnees['orientation'] == 0:
b_hg = noeud.get_branche("hg")
b_bg = noeud.get_branche("bg")
b_hg.reconnecte_noeud(noeud, "hg", noeud, "g")
b_bg.reconnecte_noeud(noeud, "bg", noeud, "d")
elif noeud.donnees['orientation'] == 180:
b_hd = noeud.get_branche("hd")
b_bd = noeud.get_branche("bd")
b_hd.reconnecte_noeud(noeud, "hd", noeud, "g")
b_bd.reconnecte_noeud(noeud, "bd", noeud, "d")
else:
assert False, "Rien à faire là"
elif noeud.type == "RelaisBasculeur":
noeuds_a_supprimer.append(nom)
rel_g = Noeud({'nom': noeud.nom + ".gauche", 'type': 'Relais',
'orientation': noeud.donnees['orientation'], 'basculeur': 'gauche'})
rel_d = Noeud({'nom': noeud.nom + ".droite", 'type': 'Relais',
'orientation': noeud.donnees['orientation'], 'basculeur': 'droite'})
noeuds_a_ajouter.append((noeud.nom + ".gauche", rel_g))
noeuds_a_ajouter.append((noeud.nom + ".droite", rel_d))
# On récupère les branches
br_hg = noeud.get_branche("hg")
br_bg = noeud.get_branche("bg")
br_hd = noeud.get_branche("hd")
br_bd = noeud.get_branche("bd")
# Puis les voisins
vois_hg, vois_hg_dir = br_hg.get_neighbour(noeud, "hg")
vois_bg, vois_bg_dir = br_bg.get_neighbour(noeud, "bg")
vois_hd, vois_hd_dir = br_hd.get_neighbour(noeud, "hd")
vois_bd, vois_bd_dir = br_bd.get_neighbour(noeud, "bd")
# On supprime les branches
cmp_hg = self.remove_branche(br_hg)
cmp_bg = self.remove_branche(br_bg)
cmp_hd = self.remove_branche(br_hd)
cmp_bd = self.remove_branche(br_bd)
# On lie les nouveaux noeuds
self.connecte(rel_g, "g", vois_hg, vois_hg_dir, cmp_hg)
self.connecte(rel_g, "d", vois_bg, vois_bg_dir, cmp_bg)
self.connecte(rel_d, "g", vois_hd, vois_hd_dir, cmp_hd)
self.connecte(rel_d, "d", vois_bd, vois_bd_dir, cmp_bd)
[self.remove_noeud(n) for n in noeuds_a_supprimer]
[self.noeuds.__setitem__(nom, valeur) for nom, valeur in noeuds_a_ajouter]
def simplifier(self):
"""
Simplifie le graphe :
- supprime les coudes
"""
# Supprime les coudes
coudes = []
for n in self.noeuds.values():
if n.type == 'Coude':
try:
a1, a2 = n.get_branches("m")
except ValueError:
raise GraphStructException("Un coude doit avoir deux voisins")
# On récupère les voisins
v1, d1 = a1.get_neighbour(n, "m")
v2, d2 = a2.get_neighbour(n, "m")
comp1 = self.remove_branche(a1)
comp2 = self.remove_branche(a2)
self.connecte(v1, d1, v2, d2, comp1 + comp2)
coudes.append(n)
[self.remove_noeud(n.nom) for n in coudes]
def traiter_condensateurs(self):
cond_list = []
for nom, noeud in self.noeuds.items():
if noeud.type == "Condensateur":
branche_g = noeud.get_branche("g")
branche_d = noeud.get_branche("d")
n1 = self.nouveau_nom()
nn_amont = Noeud({'nom': n1, 'type': 'Noeud'})
cond_list.append(nn_amont)
n2 = self.nouveau_nom()
nn_aval = Noeud({'nom': n2, 'type': 'Noeud'})
cond_list.append(nn_aval)
cond = Condensateur(noeud.donnees)
self.condensateurs[nom] = cond
cond.amont = nn_amont
cond.aval = nn_aval
# On raccorde le nouveau noeud amont au voisin amont initial
voisin_g, dir_g = branche_g.get_neighbour(noeud, "g")
composants_g = self.remove_branche(branche_g)
self.connecte(voisin_g, dir_g, nn_amont, "m", composants_g)
# Le noeud nouveau noeud amont au condensateur
self.connecte_condensateur_amont(cond, nn_amont, "m")
# Le nouveau noeud aval au condensateur
self.connecte_condensateur_aval(cond, nn_aval, "m")
# On raccorde le nouveau noeud aval à la branche aval initiale
voisin_d, dir_d = branche_d.get_neighbour(noeud, "d")
composants_d = self.remove_branche(branche_d)
self.connecte(voisin_d, dir_d, nn_aval, "m", composants_d)
for c in cond_list:
self.noeuds[c.nom] = c
def reduire_branches(self):
noeuds_a_supprimer = []
for nom, noeud in self.noeuds.items():
if noeud.type in self.REDUCTIBLE:
# Le composant est stockable dans la branche (composant simple en série)
b_gauche = noeud.get_branche("g")
b_droite = noeud.get_branche("d")
voisin_g, voisin_g_dir = b_gauche.get_neighbour(noeud, "g")
voisin_d, voisin_d_dir = b_droite.get_neighbour(noeud, "d")
comp_br_gauche = self.remove_branche(b_gauche)
comp_br_droite = self.remove_branche(b_droite)
self.connecte(voisin_g, voisin_g_dir, voisin_d, voisin_d_dir, comp_br_gauche + [noeud] + comp_br_droite)
noeuds_a_supprimer.append(nom)
if noeud.type == "Diode" and noeud.donnees['orientation'] == 180:
noeud.nom = "!" + noeud.nom
elif noeud.type == "Coude":
assert False, "Les coudes auraient dûs être éliminés !"
elif noeud.type in self.RIEN_A_FAIRE:
pass
else:
raise GraphStructException("Type inconnu : " + noeud.type)
for n in noeuds_a_supprimer:
del self.noeuds[n]
def make_liste_composants(self):
self.composants = {}
for b in self.branches:
for c in b.composants:
self.composants[c.nom] = c
def process(self):
"""
Méthode principale de traitement du graphe
"""
try:
self.simplifier()
self.traiter_composites()
self.traiter_condensateurs()
self.reduire_branches()
self.make_liste_composants()
except KeyError:
logging.exception("Erreur de chaînage dans le graphe")
raise GraphStructException

514
modele/elements.py Normal file
View File

@@ -0,0 +1,514 @@
from modele.exceptions import (WrongElecPropertyException, SurtensionException,
SurintensiteException, Dir)
class Element(object):
"""
Classe mère.
Le graphe lui-même est composé de Potentiels fixes (Potentiel), de Noeuds, et de Branches.
Les branches contiennent une liste de composants en série.
Les éléments doivent implémenter quatre méthodes pour le calcul des valeurs :
init_cycle est appelée avant chaque cycle, avant l'initialisation de la matrice
report_pass_1 est appelée après la première passe pour report des valeurs de la matrice (et avant la construction
de la matrice de la deuxième passe)
report_pass2 est appelée après la 2e passe pour reporter les valeurs finales calculées
update est appelée après le report de la 2e passe, pour pouvoir faire la mise à jour des états des éléments
"""
P_CST, P_NOEUD, P_DLT, P_COND, P_EQVAR, P_BSCLAME = range(6)
def __init__(self, conf, nom, type_potentiel):
self.type_potentiel = type_potentiel
self.conf = conf
self.nom = nom
def init_cycle(self):
pass
def passe_calcul(self, solutions, index):
pass
def update(self):
pass
def etat(self):
raise NotImplementedError
def coherence(self):
raise NotImplementedError
def eq_elts(self):
"""
Retourne les équipotentiels
Par défaut, un seul équipotentiel
"""
return [self]
def get_potentiel(self, direction):
"""
Par défaut, un seul potentiel quel que soit la direction
"""
return self
class Potentiel(Element):
"""
Potentiel à 24V ou à 0V
"""
def __init__(self, conf, nom, u=0.0, voisins=None):
super().__init__(conf, nom, self.P_CST)
self._u = u
if voisins is None:
voisins = []
self._voisins = voisins
def add_voisins(self, voisins):
self._voisins += voisins
def voisins(self):
return self._voisins
def u(self):
return self._u
def i(self):
raise WrongElecPropertyException
def coherence(self):
pass
def etat(self):
return self.u() >= self.conf['USEUIL']
class PotPulse(Potentiel):
def __init__(self, conf, nom, u=0.0, periode=None, voisins=None):
super().__init__(conf, nom, u, voisins)
if periode is None:
periode = self.conf['PULSE_PERIODE']
self.periode = periode / self.conf['DT']
self.u_max = u
self.actif = True
self.compteur = 0
def update(self):
self.compteur += 1
if self.actif:
if self.compteur >= self.periode:
self.compteur = 0
self._u = 0.
self.actif = False
else:
if self.compteur >= self.periode:
self.compteur = 0
self._u = self.u_max
self.actif = True
class Noeud(Element):
"""
Nœud du graphe
self._voisins: liste de (Graphe, polarité) avec polarité = +1 ou -1 pour les branches
"""
def __init__(self, conf, nom, voisins=None):
super().__init__(conf, nom, self.P_NOEUD)
self._u = None
if voisins is None:
voisins = []
self._voisins = voisins
def add_voisins(self, voisins):
self._voisins += voisins
def voisins(self):
return self._voisins
def passe_calcul(self, vect, index):
super().passe_calcul(vect, index)
self._u = vect[index]
def u(self):
return self._u
def coherence(self):
if abs(self.u()) >= self.conf['UMAX']:
raise SurtensionException
def etat(self):
return self.u() >= self.conf['USEUIL']
class Branche(Element):
"""
Branche du graphe.
La branche contient un certain nombre de composants en série, qui peuvent être des relais, tempos, résistances
ou contacts (équation). N'est pas utilisable pour les condensateurs.
"""
def __init__(self, conf, nom, composants):
super().__init__(conf, nom, self.P_DLT)
self.composants = composants
self.amont = None
self.aval = None
self._i = 0.0
def add_voisins(self, amont, aval):
self.amont = amont
self.aval = aval
def r(self):
return sum([x.r for x in self.composants]) + self.conf['RMIN']
def init_cycle(self):
super().init_cycle()
[x.init_cycle() for x in self.composants]
def passe_calcul(self, vect, index):
super().passe_calcul(vect, index)
self._i = vect[index] # Important de mettre à jour i avant de mettre à jour les composants
[x.passe_calcul() for x in self.composants]
def update(self):
super().update()
[x.update() for x in self.composants]
def i(self):
return self._i
def coherence(self):
if abs(self.i()) >= self.conf['IMAX']:
raise SurintensiteException(f"Dans la branche {self.nom}")
def etat(self):
return self.i() >= self.conf['ISEUIL']
class Condensateur(Element):
"""
Condensateur.
La seule truande est que la tension au borne est en réalité calculée avec la charge du cycle précédent.
"""
def __init__(self, conf, nom, capacite, coef_mul_tension):
super().__init__(conf, nom, self.P_COND)
self.capacite = capacite
self.amont = None
self.aval = None
self._i = 0.0
self.q = 0.0
self.q_prec = 0.0
self.coef_mul_tension = coef_mul_tension
def add_voisins(self, amont, aval):
self.amont = amont
self.aval = aval
def r(self):
raise WrongElecPropertyException
def init_cycle(self):
super().init_cycle()
def passe_calcul(self, vect, index):
super().passe_calcul(vect, index)
self._i = vect[index] # Important de mettre à jour i avant de mettre à jour les composants
def update(self):
super().update()
self.q_prec = self.q
q = self.q + self.i() * self.conf['DT']
self.q = q
def i(self):
return self._i
def coherence(self):
if abs(self.i()) >= self.conf['IMAX']:
raise SurintensiteException
def etat(self):
return bool(self.i() >= self.conf['ISEUIL'])
class ContactBasculeur(Noeud):
"""
Nœud du graphe
self._voisins: liste de (Graphe, polarité) avec polarité = +1 ou -1 pour les branches
"""
UMAX = 24.5
class PotentielLocal(Element):
def __init__(self, conf, nom, voisins=None):
self.lame = None
super().__init__(conf, nom, self.P_NOEUD)
self._u = None
if voisins is None:
voisins = []
self._voisins = voisins
self.etat = False
def set_lame(self, lame):
self.lame = lame
def add_voisins(self, voisins):
self._voisins += voisins
def set_etat(self, etat):
self.etat = etat
def voisins(self):
return self._voisins + ([(self.lame, +1, "foo")] if self.etat else [])
def passe_calcul(self, vect, index):
super().passe_calcul(vect, index)
self._u = vect[index]
def u(self):
return self._u
def coherence(self):
if abs(self.u()) >= self.conf['UMAX']:
raise SurtensionException
class Lame(Element):
def __init__(self, conf, nom, contact, gauche, droite):
self.amont = contact
self.aval = None
self.gauche = gauche
self.droite = droite
self.position = None
super().__init__(conf, nom, self.P_DLT)
self._i = None
def bascule(self, position):
self.position = position
if position == Dir.GAUCHE:
self.aval = self.gauche
else:
self.aval = self.droite
def i(self):
return self._i
def r(self):
return self.conf['RMIN']
# Classe principale
def __init__(self, conf, nom, graphe, voisins=None):
self.graphe = graphe
self.position = None
self.potentiel_gauche = self.PotentielLocal(conf, nom + ".G")
self.potentiel_droit = self.PotentielLocal(conf, nom + ".D")
self.lame = self.Lame(conf, nom + ".L", self, self.potentiel_gauche, self.potentiel_droit)
self.potentiel_gauche.set_lame(self.lame)
self.potentiel_droit.set_lame(self.lame)
self._voisins = [(self.lame, -1, "foo")] + voisins if voisins is not None else []
super().__init__(conf, nom, self.P_NOEUD)
self.bascule_gauche()
def eq_elts(self):
return [self, self.potentiel_gauche, self.potentiel_droit, self.lame]
def add_voisins(self, voisins):
for vois_tuple in voisins:
_, _, connecteur = vois_tuple
if connecteur == 'b':
self._voisins = [vois_tuple]
else:
# Les autres connecteurs doivent être câblés vers les ConnecteursLocaux
raise "Invalid connector for RelaisBasculeur"
def voisins(self):
return self._voisins
def bascule_gauche(self):
self.position = Dir.GAUCHE
self.lame.bascule(self.position)
self.potentiel_gauche.set_etat(True)
self.potentiel_droit.set_etat(False)
def bascule_droite(self):
self.position = Dir.DROITE
self.lame.bascule(self.position)
self.potentiel_gauche.set_etat(False)
self.potentiel_droit.set_etat(True)
def get_potentiel(self, direction):
"""
Par défaut, un seul potentiel quel que soit la direction
"""
if direction == "b":
return self
elif direction == "g":
return self.potentiel_gauche
elif direction == "d":
return self.potentiel_droit
else:
assert False
def passe_calcul(self, vect, index):
super().passe_calcul(vect, index)
self.potentiel_droit.passe_calcul(vect, self.graphe.vecteur.index(self.potentiel_droit))
self.potentiel_gauche.passe_calcul(vect, self.graphe.vecteur.index(self.potentiel_gauche))
self._u = vect[index]
def u(self):
return self._u
def etat(self):
return self.lame.position
def coherence(self):
if abs(self.u()) >= self.conf['UMAX']:
raise SurtensionException
self.potentiel_droit.coherence()
self.potentiel_gauche.coherence()
class ContactDouble(Noeud):
"""
Nœud du graphe
self._voisins: liste de (Graphe, polarité) avec polarité = +1 ou -1 pour les branches
"""
UMAX = 24.5
class PotentielLocal(Element):
def __init__(self, conf, nom, voisins=None):
self.lame = None
super().__init__(conf, nom, self.P_NOEUD)
self._u = None
if voisins is None:
voisins = []
self._voisins = voisins
self.etat = False
def set_lame(self, lame):
self.lame = lame
def add_voisins(self, voisins):
self._voisins += voisins
def set_etat(self, etat):
self.etat = etat
def voisins(self):
return self._voisins + ([(self.lame, +1, "foo")] if self.etat else [])
def passe_calcul(self, vect, index):
super().passe_calcul(vect, index)
self._u = vect[index]
def u(self):
return self._u
def coherence(self):
if abs(self.u()) >= self.conf['UMAX']:
raise SurtensionException
class Lame(Element):
def __init__(self, conf, nom, contact, gauche, droite):
self.amont = contact
self.aval = None
self.repos = gauche
self.travail = droite
self.position = False # Travail = True
super().__init__(conf, nom, self.P_DLT)
self._i = None
def set_position(self, position):
self.position = position
if position:
self.aval = self.travail
else:
self.aval = self.repos
def i(self):
return self._i
def r(self):
return self.conf['RMIN']
# Classe principale
def __init__(self, conf, nom, graphe, voisins=None, orientation=0):
self.graphe = graphe
self.orientation = orientation
self.potentiel_repos = self.PotentielLocal(conf, nom + ".repos")
self.potentiel_travail = self.PotentielLocal(conf, nom + ".travail")
self.lame = self.Lame(conf, nom + ".L", self, self.potentiel_repos, self.potentiel_travail)
self.potentiel_repos.set_lame(self.lame)
self.potentiel_travail.set_lame(self.lame)
self._voisins = [(self.lame, -1, "foo")] + voisins if voisins is not None else []
super().__init__(conf, nom, self.P_NOEUD)
self.lame.set_position(False)
def eq_elts(self):
return [self, self.potentiel_repos, self.potentiel_travail, self.lame]
def add_voisins(self, voisins):
for vois_tuple in voisins:
_, _, connecteur = vois_tuple
if (self.orientation == 0 and connecteur == 'd') \
or (self.orientation == 180 and connecteur == 'g'):
self._voisins = [vois_tuple]
else:
# Les autres connecteurs doivent être câblés vers les ConnecteursLocaux
raise "Invalid connector for ContactDouble"
def voisins(self):
return self._voisins
def get_potentiel(self, direction):
"""
Par défaut, un seul potentiel quel que soit la direction
"""
if self.orientation == 0:
if direction == "d":
return self
elif direction == "bg":
return self.potentiel_repos
elif direction == "hg":
return self.potentiel_travail
else:
assert False
elif self.orientation == 180:
if direction == "g":
return self
elif direction == "bd":
return self.potentiel_repos
elif direction == "hd":
return self.potentiel_travail
else:
assert False
else:
assert False
def passe_calcul(self, vect, index):
super().passe_calcul(vect, index)
self.potentiel_travail.passe_calcul(vect, self.graphe.vecteur.index(self.potentiel_travail))
self.potentiel_repos.passe_calcul(vect, self.graphe.vecteur.index(self.potentiel_repos))
self._u = vect[index]
def u(self):
return self._u
def etat(self):
return self.lame.position
def coherence(self):
if abs(self.u()) >= self.conf['UMAX']:
raise SurtensionException
self.potentiel_travail.coherence()
self.potentiel_repos.coherence()
def active(self):
self.lame.set_position(True)
self.potentiel_travail.set_etat(True)
self.potentiel_repos.set_etat(False)
def desactive(self):
self.lame.set_position(False)
self.potentiel_travail.set_etat(False)
self.potentiel_repos.set_etat(True)

22
modele/exceptions.py Normal file
View File

@@ -0,0 +1,22 @@
from enum import IntEnum
class WrongElecPropertyException(Exception):
pass
class NonPolariseException(Exception):
pass
class SurtensionException(Exception):
pass
class SurintensiteException(Exception):
pass
class Dir(IntEnum):
GAUCHE = 1
DROITE = 2

171
modele/graphe.py Normal file
View File

@@ -0,0 +1,171 @@
import modele.composants as mc
import modele.elements as me
from modele.exceptions import Dir
from modele.donnees import GraphStructException
class Vecteur(object):
"""
Associe les éléments du graphe à leur rang dans le vecteur correspondant aux rangs dans la matrice.
"""
def __init__(self, elements):
self._liste = [eq_elt for element in elements.values() for eq_elt in element.eq_elts()]
self._index = {} # Associe à un élément son index dans la matrice
for index, element in enumerate(self._liste):
self._index[element] = index
def __len__(self):
return len(self._liste)
def index(self, element):
return self._index[element]
def __iter__(self):
return self._liste.__iter__()
class Graphe(object):
ID_BR = -1
@classmethod
def branch_id(cls):
cls.ID_BR += 1
return f"b{cls.ID_BR}"
def __init__(self, conf):
self.conf = conf
self.elements = None
self.composants = None
self.vecteur = None
def load_data_from_schema_reader(self, reader):
self.elements = {}
self.composants = {}
# Construction des éléments et composants
for nom, valeur in reader.composants.items():
if valeur.type == 'Bouton' or valeur.type == 'ContactTravail' or valeur.type == 'ContactRepos' \
or valeur.type == 'Levier':
self.composants[nom] = mc.Contact(self.conf, nom, valeur.type)
elif valeur.type == 'Relais':
if valeur.donnees.get('basculeur') == 'gauche':
self.composants[nom] = mc.DemiRelaisBasculeur(self.conf, nom, self, Dir.GAUCHE)
elif valeur.donnees.get('basculeur') == 'droite':
self.composants[nom] = mc.DemiRelaisBasculeur(self.conf, nom, self, Dir.DROITE)
else:
self.composants[nom] = mc.Relais(self.conf, nom, self)
elif valeur.type == 'Tempo':
self.composants[nom] = mc.Tempo(self.conf, nom, valeur.donnees['tempo_blocage'],
valeur.donnees['tempo_liberation'])
elif valeur.type == 'Resistance':
if 'valeur' not in valeur.donnees:
v = None
else:
v = valeur.donnees['valeur']
self.composants[nom] = mc.Resistance(self.conf, nom, v)
del v
elif valeur.type == 'Lampe':
self.composants[nom] = mc.Lampe(self.conf, nom)
elif valeur.type == 'Diode':
d = mc.Diode(self.conf, nom)
self.composants[nom] = d
self.composants["!" + nom] = d
del d
for nom, valeur in reader.noeuds.items():
if valeur.type == "Noeud":
self.elements[nom] = me.Noeud(self.conf, nom)
elif valeur.type == "P24":
self.elements[nom] = me.Potentiel(self.conf, nom, 24.)
elif valeur.type == "P0":
self.elements[nom] = me.Potentiel(self.conf, nom, 0.)
elif valeur.type == "Pulse":
if "periode" not in valeur.donnees:
p = None
else:
p = valeur.donnees["periode"]
self.elements[nom] = me.PotPulse(self.conf, nom, 24., p)
elif valeur.type == "ContactBasculeur":
self.elements[nom] = me.ContactBasculeur(self.conf, nom, self)
elif valeur.type == "ContactDouble":
self.elements[nom] = me.ContactDouble(self.conf, nom, self, orientation=valeur.donnees['orientation'])
for nom, valeur in reader.condensateurs.items():
cnd = me.Condensateur(self.conf, nom, valeur.donnees['capacite'], valeur.donnees['coef_mul_tension'])
v_amont_schm, v_amont_dir = valeur.get_amont()
v_aval_schm, v_aval_dir = valeur.get_aval()
v_amont = self.elements[v_amont_schm.nom]
v_aval = self.elements[v_aval_schm.nom]
cnd.add_voisins(v_amont, v_aval)
v_amont.add_voisins([(cnd, -1, v_amont_dir)])
v_aval.add_voisins([(cnd, +1, v_aval_dir)])
self.elements[nom] = cnd
del cnd
# Ajout des liens entre relais et contacts
try:
for nom, valeur in reader.composants.items():
if valeur.type == "ContactTravail":
self.composants[valeur.donnees["relais"]].add_travail(nom)
elif valeur.type == "ContactRepos":
self.composants[valeur.donnees["relais"]].add_repos(nom)
for nom, valeur in reader.noeuds.items():
if valeur.type == "ContactBasculeur":
self.composants[valeur.donnees["relais"] + ".gauche"].add_contact(nom)
self.composants[valeur.donnees["relais"] + ".droite"].add_contact(nom)
elif valeur.type == "ContactDouble":
self.composants[valeur.donnees["relais"]].add_double(nom)
except KeyError:
raise GraphStructException("Mauvais identificateur de relais dans un contact")
# Construction des branches
for branche in reader.branches:
nom = self.branch_id()
br = me.Branche(self.conf, nom, [self.composants[x.nom] for x in branche.composants])
[self.composants[x.nom].reverse() for x in branche.composants if x.nom[0] == '!'] # Diode
for c in br.composants:
c.branche = br
v_amont_schm, v_amont_dir = branche.get_amont()
v_aval_schm, v_aval_dir = branche.get_aval()
v_amont = self.elements[v_amont_schm.nom].get_potentiel(v_amont_dir)
v_aval = self.elements[v_aval_schm.nom].get_potentiel(v_aval_dir)
br.add_voisins(v_amont, v_aval)
v_amont.add_voisins([(br, -1, v_amont_dir)])
v_aval.add_voisins([(br, +1, v_aval_dir)])
self.elements[nom] = br
del br
self.vecteur = Vecteur(self.elements)
def __getitem__(self, item):
"""
Retourne un composant
"""
if item in self.composants:
res = self.composants[item]
else:
res = self.elements[item]
return res
def __contains__(self, item):
return item in self.composants or item in self.elements
def init_cycle(self):
[elt.init_cycle() for elt in self.elements.values()]
def passe_calcul(self, solutions):
[elt.passe_calcul(solutions, self.vecteur.index(elt)) for elt in self.elements.values()]
def update(self):
[elt.update() for elt in self.elements.values()]
def coherence(self):
[x.coherence() for x in self.elements.values()]

57
modele/matrice.py Normal file
View File

@@ -0,0 +1,57 @@
import numpy as np
class Matrice(object):
"""
Le système d'équation est construit sur la base d'une matrice A et un vecteur B modélisant le système, tels que
A.X = B
Chaque ligne de la matrice correspond à une équation, correspondant elle-même à un élément du graphe.
Les équations sont dans le même ordre que les éléments du graphe.
Chaque colonne correspond à une inconnue, également prise dans le même ordre que les éléments du graphe.
Si l'élément est un Potentiel ou un noeud, l'inconnue est une tension.
Si l'élément est une branche, l'inconnue est l'intensité parcourant la branche.
"""
def __init__(self, graphe):
self.vecteur = graphe.vecteur
self.mat_A = None
self.mat_B = None # Equation matricielle AX = B
self.n = len(self.vecteur)
def init_mat(self):
self.mat_A = np.zeros((self.n, self.n))
self.mat_B = np.zeros(self.n)
for ligne, element in enumerate(self.vecteur): # Potentiel constant
# assert ligne == self.vecteur.index(element)
if element.type_potentiel == element.P_CST:
# Potentiel constant : on ajoute une équation indiquant la valeur de la tension pour cette variable
self.mat_A[ligne][ligne] = 1.0 # à noter que par construction, ligne == index
self.mat_B[ligne] = element.u()
elif element.type_potentiel == element.P_NOEUD: # On applique la loi des noeuds
# Noeud : on ajoute une équation sur la somme des intensités
for (voisin, polarite, _) in element.voisins():
self.mat_A[ligne][self.vecteur.index(voisin)] = polarite
self.mat_B[ligne] = 0.0
elif element.type_potentiel == element.P_DLT: # On applique la loi d'Ohm
self.mat_A[ligne][ligne] = 1.0
self.mat_A[ligne][self.vecteur.index(element.amont)] = -1.0 / element.r()
self.mat_A[ligne][self.vecteur.index(element.aval)] = 1.0 / element.r()
self.mat_B[ligne] = 0.0
elif element.type_potentiel == element.P_COND: # DU = q/C
self.mat_A[ligne][self.vecteur.index(element.amont)] = 1.0
self.mat_A[ligne][self.vecteur.index(element.aval)] = -1.0
self.mat_B[ligne] = element.coef_mul_tension * element.q / element.capacite
else:
assert False, "Rien à faire là"
def solve(self):
"""
Résout le système d'équation et retourne le vecteur solution.
"""
return np.linalg.solve(self.mat_A, self.mat_B)