diff --git a/api_server/__pycache__/__init__.cpython-312.pyc b/api_server/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..03083f3 Binary files /dev/null and b/api_server/__pycache__/__init__.cpython-312.pyc differ diff --git a/api_server/__pycache__/app.cpython-312.pyc b/api_server/__pycache__/app.cpython-312.pyc new file mode 100644 index 0000000..5d83b0a Binary files /dev/null and b/api_server/__pycache__/app.cpython-312.pyc differ diff --git a/api_server/__pycache__/state.cpython-312.pyc b/api_server/__pycache__/state.cpython-312.pyc new file mode 100644 index 0000000..71d9a07 Binary files /dev/null and b/api_server/__pycache__/state.cpython-312.pyc differ diff --git a/api_server/app.py b/api_server/app.py new file mode 100644 index 0000000..f5cea62 --- /dev/null +++ b/api_server/app.py @@ -0,0 +1,218 @@ +""" +Lancer avec python -m flask --debug --app server\\__init__.py run +""" +import logging +import sys +import time + +from flask import Flask, request, session +import multiprocessing as mp +from api_worker.__init__ import run_instance +import uuid +from flask_cors import CORS + +app = Flask("COCOSIM") +CORS(app, supports_credentials=True) +app.secret_key = bytes.fromhex('192b9bdd22ab9ed4d12e236c78afcb9a393ec15f71bbf5dc987d54727823bcbf') + + +class ProcessBase(object): + SESSION_MAX_AGE = 60 * 5 + + def __init__(self): + self._cmd_q = {} + self._state_q = {} + self._process = {} + self._last = {} + + def known(self): + return 'id' in session and session['id'] in self._cmd_q + + def update(self): + self._last[session['id']] = time.time() + + def cmd_q(self): + self.update() + return self._cmd_q[session['id']] + + def state_q(self): + self.update() + return self._state_q[session['id']] + + def init_q(self): + self.update() + self._state_q[session['id']] = mp.Queue() + self._cmd_q[session['id']] = mp.Queue() + + def init_process(self, p): + self.update() + self.cleanup() + self._process[session['id']] = p + + def process(self): + return self._process[session['id']] + + def purge(self, k=None): + if k is None: + k = session['id'] + + del self._cmd_q[k] + del self._state_q[k] + del self._process[k] + del self._last[k] + + def quit(self): + self.purge() + self.cleanup() + + def cleanup(self): + ctime = time.time() + to_purge = [] + + for k, v in self._last.items(): + if ctime - v > self.SESSION_MAX_AGE: + to_purge.append(k) + + for k in to_purge: + logging.info(f"Purging {k}") + self.purge(k) + + def __str__(self): + return str(list(self._process.keys())) + + def __len__(self): + return len(self._process.keys()) + + +PROCESS_BASE = ProcessBase() + + +@app.route('/') +def hello(): + PROCESS_BASE.cleanup() + return f"Hello sailor !\nProcess base : {str(PROCESS_BASE)}" + + +@app.route('/init') +def init(): + session['id'] = uuid.uuid4().int + PROCESS_BASE.init_q() + PROCESS_BASE.init_process(mp.Process(target=run_instance, args=(session['id'], PROCESS_BASE.cmd_q(), PROCESS_BASE.state_q()))) + PROCESS_BASE.process().start() + return "OK" + + +@app.route('/start') +def start(): + if not PROCESS_BASE.known(): + return "Unknown", 401 + PROCESS_BASE.cmd_q().put({'cmd': "start"}) + return "OK" + + +@app.route('/stop') +def stop(): + if not PROCESS_BASE.known(): + return "Unknown", 401 + + PROCESS_BASE.cmd_q().put({'cmd': "stop"}) + return "OK" + + +@app.route('/state') +def state(): + if not PROCESS_BASE.known(): + return "Unknown", 401 + + PROCESS_BASE.cmd_q().put({'cmd': "state"}) + data = PROCESS_BASE.state_q().get() + return data + + +@app.route('/quit') +def quit(): + if not PROCESS_BASE.known(): + PROCESS_BASE.cleanup() + return "Unknown", 401 + + PROCESS_BASE.cmd_q().put({'cmd': "quit"}) + PROCESS_BASE.process().join() + PROCESS_BASE.quit() + del session['id'] + logging.info("Purge effectuée") + return "OK" + + +@app.route('/load', methods=['POST']) +def load(): + if not PROCESS_BASE.known(): + return "Unknown", 401 + + try: + data = request.json + PROCESS_BASE.cmd_q().put({'cmd': "load", "args": data}) + except Exception: + logging.exception("ERROR") + return "Error", 418 + + if PROCESS_BASE.state_q().get() == "OK": + return "OK" + else: + return "Error in graph", 418 + + +@app.route('/push/') +def push(obj): + """ + Push button + """ + if not PROCESS_BASE.known(): + return "Unknown", 401 + + PROCESS_BASE.cmd_q().put({'cmd': "push", "args": obj}) + return "OK" + + +@app.route('/release/') +def release(obj): + """ + Release button + """ + if not PROCESS_BASE.known(): + return "Unknown", 401 + + PROCESS_BASE.cmd_q().put({'cmd': "release", "args": obj}) + return "OK" + + +def just_run(): + """ + Pour lancer avec gunicorn + """ + mp.set_start_method('spawn') + app.run() # Ne pas mettre debug en mode multiprocess + + +def main(argv): + host = '127.0.0.1' + ssl_context = None + for arg in argv[1:]: + if arg == '-remote': + host = '0.0.0.0' + elif arg == '-secure-cookie': + app.config.update(SESSION_COOKIE_SAMESITE="None", SESSION_COOKIE_SECURE="True") + elif arg == '-ssl': + ssl_context = 'adhoc' + elif arg == '-h': + print(""" +Options : + -remote : bind on 0.0.0.0 instead of 127.0.0.1 + -secure-cookie : send secure cookie (for browser UI to be happy) + -ssl : enable https with adhoc certificate +""") + sys.exit(0) + else: + print(f"Unknown argument : {arg}") + sys.exit(-1) + mp.set_start_method('spawn') + app.run(debug=False, host=host, port=5555, ssl_context=ssl_context) # Ne pas mettre debug en mode multiprocess diff --git a/api_server/state.py b/api_server/state.py new file mode 100644 index 0000000..9cba09c --- /dev/null +++ b/api_server/state.py @@ -0,0 +1,41 @@ +import json + + +class State(object): + IGNORE = frozenset(['Coude', 'Noeud', 'P24', 'P0', 'Pulse']) + + def __init__(self, cocosim_server): + self.cocosim_server = cocosim_server + self.conf = self.cocosim_server.conf["ui"] + self.composants = {} + self._exception = None + + def load_conf(self, schema_d): + # Chargement des blocs + for nom, valeur in schema_d['blocs'].items(): + if valeur['type'] not in self.IGNORE: + self.composants[nom] = None + + def to_json(self): + if self._exception: + return json.dumps({'exn': self._exception}) + else: + return json.dumps(self.composants) + + def __iter__(self): + return self.conf.__iter__ + + def items(self): + return self.composants.items() + + def keys(self): + return self.composants.keys() + + def update(self, nom, valeur): + self.composants[nom] = valeur + + def exception(self, e): + self._exception = e + + def reset_exception(self): + self._exception = None \ No newline at end of file diff --git a/api_server/test_client.py b/api_server/test_client.py new file mode 100644 index 0000000..fe72172 --- /dev/null +++ b/api_server/test_client.py @@ -0,0 +1,19 @@ +import asyncio + + +async def tcp_client(): + reader, writer = await asyncio.open_connection( + '127.0.0.1', 8888) + + while not writer.is_closing(): + message = input("> ") + print(f'Send: {message!r}') + writer.write(message.encode()) + await writer.drain() + if message == "state": + data = await reader.read(1000) + else: + data = await reader.read(100) + print(f'Received: {data.decode()!r}') + +asyncio.run(tcp_client()) \ No newline at end of file diff --git a/api_worker/__init__.py b/api_worker/__init__.py new file mode 100644 index 0000000..4668ecb --- /dev/null +++ b/api_worker/__init__.py @@ -0,0 +1,135 @@ +import json +import asyncio +from modele.donnees import SchemaReader, GraphStructException +from modele.graphe import Graphe +from modele.cocosim import Cocosim +from api_server.state import State +import logging +import modele.exceptions + +class CocosimInstance(object): + def __init__(self, cmd_q, state_q): + self.runner = None + try: + conf_fp = open("modele/config.json") + conf = json.load(conf_fp) + + self.conf = conf + self.dt = self.conf['circuit']['DT'] + self.cocosim = None + self.state = None + self.cmd_q = cmd_q + self.state_q = state_q + self.quit = False + except (OSError, json.JSONDecodeError) as e: + logging.exception("Reading conf. file") + raise modele.G + + def setup(self, schema_d): + logging.info("Setting up") + reader = SchemaReader(schema_d) + reader.process() + logging.info("Schema is loaded") + self.state = State(self) + self.state.load_conf(schema_d) + logging.info(f"State is made up with {len(self.state.composants)} items.") + graphe = Graphe(self.conf['circuit']) + graphe.load_data_from_schema_reader(reader) + logging.info("Graph is initialized.") + self.cocosim = Cocosim(self.conf, graphe, {}, {}) + logging.info("Cocosim is initialized") + + async def handler(self): + loop = asyncio.get_running_loop() + data = await loop.run_in_executor(None, self.cmd_q.get) + message = data['cmd'] + try: + if message == "start": + logging.log(logging.INFO, "Starting") + self.runner = asyncio.create_task(self.run()) + elif message == "stop": + logging.log(logging.INFO, "Stopping") + await self.stop() + elif message == "state": + self.state_q.put(self.state.to_json()) + elif message == "quit": + logging.log(logging.INFO, "Quitting") + await self.stop() + self.quit = True + elif message == "load": + logging.log(logging.INFO, "Loading file") + try: + self.setup(data['args'] if type(data['args']) == dict else json.loads(data['args'])) # Compatibilité avec certaines IHM + self.state_q.put("OK") + except GraphStructException: + self.state_q.put("Error") + elif message == "push": + but_name = data['args'] + logging.log(logging.INFO, f"Push button {but_name}") + if self.cocosim.graphe[but_name] == "Bouton": + self.cocosim.graphe[but_name].ferme() + else: + self.cocosim.graphe[but_name].bascule() + elif message == "release": + but_name = data['args'] + logging.log(logging.INFO, f"Release button {but_name}") + if self.cocosim.graphe[but_name].type == "Bouton": + self.cocosim.graphe[but_name].ouvre() + # Dans le cas contraire on ne traite pas le release + else: + logging.log(logging.ERROR, "Commande inconnue") + except Exception: + logging.exception("Exception occured in handler") + + def cycle(self): + try: + self.cocosim.cycle() + + for nom in self.state.keys(): + if nom not in self.cocosim.graphe and nom[-7:] == ".double": # TODO : a supprimer apràs refactoring contact double + nom_translate = nom[:-7] + ".travail" + value = self.cocosim.graphe[nom_translate].etat() if nom_translate in self.cocosim.graphe else None + self.state.update(nom, value) + else: + value = self.cocosim.graphe[nom].etat() if nom in self.cocosim.graphe else None + self.state.update(nom, value) + except modele.base.SurintensiteException as e: + self.state.exception("Surintensité") + raise e + except modele.base.SurtensionException as e: + self.state.exception("Surtension") + raise e + + + async def run(self): + try: + while True: + self.state.reset_exception() + self.cycle() + await asyncio.sleep(self.dt) + except: + logging.exception("Exception occured in 'cycle'") + + async def stop(self): + if self.runner: + self.runner.cancel() + try: + await self.runner + except asyncio.CancelledError: + logging.info("Runner is stopped.") + self.runner = None + + async def handle_requests(self): + while not self.quit: + await self.handler() + logging.info("Stop handling request") + + def __del__(self): + logging.info("Cocosim instance destroyed") + + +def run_instance(uid, cmd_q, state_q): + logging.basicConfig(filename=f"cocosim.log", filemode="w", level=logging.INFO) + logging.info("Creating instance") + i = CocosimInstance(cmd_q, state_q) + asyncio.run(i.handle_requests()) diff --git a/api_worker/__pycache__/__init__.cpython-312.pyc b/api_worker/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..3f974ce Binary files /dev/null and b/api_worker/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/models.py b/app/models.py deleted file mode 100644 index e69de29..0000000 diff --git a/app/routes.py b/app/routes.py deleted file mode 100644 index e69de29..0000000 diff --git a/cocosim.log b/cocosim.log new file mode 100644 index 0000000..32f7cbb --- /dev/null +++ b/cocosim.log @@ -0,0 +1,19 @@ +INFO:root:Creating instance +INFO:root:Loading file +INFO:root:Setting up +INFO:root:Schema is loaded +INFO:root:State is made up with 5 items. +INFO:root:Graph is initialized. +INFO:root:Cocosim is initialized +INFO:root:Starting +INFO:root:Push button b +INFO:root:Release button b +INFO:root:Stopping +ERROR:root:Exception occured in 'cycle' +Traceback (most recent call last): + File "C:\Users\0203209E\Downloads\gittea\cocosim_python\api_worker\__init__.py", line 109, in run + await asyncio.sleep(self.dt) + File "C:\Program Files\Python312\Lib\asyncio\tasks.py", line 655, in sleep + return await future + ^^^^^^^^^^^^ +asyncio.exceptions.CancelledError diff --git a/images_v1/CoCoSIM_icon.png b/images_v1/CoCoSIM_icon.png new file mode 100644 index 0000000..b7210a6 Binary files /dev/null and b/images_v1/CoCoSIM_icon.png differ diff --git a/images_v1/CoCoSIM_icon.svg b/images_v1/CoCoSIM_icon.svg new file mode 100644 index 0000000..f73acb9 --- /dev/null +++ b/images_v1/CoCoSIM_icon.svg @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + diff --git a/images_v1/CoCoSIM_logo_01.png b/images_v1/CoCoSIM_logo_01.png new file mode 100644 index 0000000..084f46b Binary files /dev/null and b/images_v1/CoCoSIM_logo_01.png differ diff --git a/images_v1/CoCoSIM_logo_01.svg b/images_v1/CoCoSIM_logo_01.svg new file mode 100644 index 0000000..8846111 --- /dev/null +++ b/images_v1/CoCoSIM_logo_01.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/bouton_ferme.png b/images_v1/bouton_ferme.png new file mode 100644 index 0000000..79a20c5 Binary files /dev/null and b/images_v1/bouton_ferme.png differ diff --git a/images_v1/bouton_ferme.svg b/images_v1/bouton_ferme.svg new file mode 100644 index 0000000..2b4f545 --- /dev/null +++ b/images_v1/bouton_ferme.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/images_v1/bouton_ouvert.png b/images_v1/bouton_ouvert.png new file mode 100644 index 0000000..2b60508 Binary files /dev/null and b/images_v1/bouton_ouvert.png differ diff --git a/images_v1/bouton_ouvert.svg b/images_v1/bouton_ouvert.svg new file mode 100644 index 0000000..921109c --- /dev/null +++ b/images_v1/bouton_ouvert.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/condensateur.png b/images_v1/condensateur.png new file mode 100644 index 0000000..ba1cfa2 Binary files /dev/null and b/images_v1/condensateur.png differ diff --git a/images_v1/condensateur.svg b/images_v1/condensateur.svg new file mode 100644 index 0000000..d2c0a8a --- /dev/null +++ b/images_v1/condensateur.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/contact_basculeur_droite.png b/images_v1/contact_basculeur_droite.png new file mode 100644 index 0000000..f79c419 Binary files /dev/null and b/images_v1/contact_basculeur_droite.png differ diff --git a/images_v1/contact_basculeur_droite.svg b/images_v1/contact_basculeur_droite.svg new file mode 100644 index 0000000..119411f --- /dev/null +++ b/images_v1/contact_basculeur_droite.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/contact_basculeur_gauche.png b/images_v1/contact_basculeur_gauche.png new file mode 100644 index 0000000..f0340e5 Binary files /dev/null and b/images_v1/contact_basculeur_gauche.png differ diff --git a/images_v1/contact_basculeur_gauche.svg b/images_v1/contact_basculeur_gauche.svg new file mode 100644 index 0000000..b43be97 --- /dev/null +++ b/images_v1/contact_basculeur_gauche.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/contact_double_repos.png b/images_v1/contact_double_repos.png new file mode 100644 index 0000000..2b9942e Binary files /dev/null and b/images_v1/contact_double_repos.png differ diff --git a/images_v1/contact_double_repos.svg b/images_v1/contact_double_repos.svg new file mode 100644 index 0000000..9fe5dfe --- /dev/null +++ b/images_v1/contact_double_repos.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/contact_double_travail.png b/images_v1/contact_double_travail.png new file mode 100644 index 0000000..246661d Binary files /dev/null and b/images_v1/contact_double_travail.png differ diff --git a/images_v1/contact_double_travail.svg b/images_v1/contact_double_travail.svg new file mode 100644 index 0000000..ca18dfc --- /dev/null +++ b/images_v1/contact_double_travail.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/diode_dg.png b/images_v1/diode_dg.png new file mode 100644 index 0000000..0e2e8ec Binary files /dev/null and b/images_v1/diode_dg.png differ diff --git a/images_v1/diode_dg.svg b/images_v1/diode_dg.svg new file mode 100644 index 0000000..12a7ff1 --- /dev/null +++ b/images_v1/diode_dg.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/diode_gd.png b/images_v1/diode_gd.png new file mode 100644 index 0000000..96c0273 Binary files /dev/null and b/images_v1/diode_gd.png differ diff --git a/images_v1/diode_gd.svg b/images_v1/diode_gd.svg new file mode 100644 index 0000000..718332e --- /dev/null +++ b/images_v1/diode_gd.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/lampe.png b/images_v1/lampe.png new file mode 100644 index 0000000..cef67ff Binary files /dev/null and b/images_v1/lampe.png differ diff --git a/images_v1/lampe.svg b/images_v1/lampe.svg new file mode 100644 index 0000000..f5645c7 --- /dev/null +++ b/images_v1/lampe.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/levier_ferme.png b/images_v1/levier_ferme.png new file mode 100644 index 0000000..37d579a Binary files /dev/null and b/images_v1/levier_ferme.png differ diff --git a/images_v1/levier_ferme.svg b/images_v1/levier_ferme.svg new file mode 100644 index 0000000..a89733b --- /dev/null +++ b/images_v1/levier_ferme.svg @@ -0,0 +1,100 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/levier_ouvert.png b/images_v1/levier_ouvert.png new file mode 100644 index 0000000..49bb60a Binary files /dev/null and b/images_v1/levier_ouvert.png differ diff --git a/images_v1/levier_ouvert.svg b/images_v1/levier_ouvert.svg new file mode 100644 index 0000000..bf2c9b2 --- /dev/null +++ b/images_v1/levier_ouvert.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/mirror_bouton_ferme.png b/images_v1/mirror_bouton_ferme.png new file mode 100644 index 0000000..6c3b0e7 Binary files /dev/null and b/images_v1/mirror_bouton_ferme.png differ diff --git a/images_v1/mirror_bouton_ouvert.png b/images_v1/mirror_bouton_ouvert.png new file mode 100644 index 0000000..4e696d7 Binary files /dev/null and b/images_v1/mirror_bouton_ouvert.png differ diff --git a/images_v1/mirror_contact_double_repos.png b/images_v1/mirror_contact_double_repos.png new file mode 100644 index 0000000..c2f515a Binary files /dev/null and b/images_v1/mirror_contact_double_repos.png differ diff --git a/images_v1/mirror_contact_double_travail.png b/images_v1/mirror_contact_double_travail.png new file mode 100644 index 0000000..53f1842 Binary files /dev/null and b/images_v1/mirror_contact_double_travail.png differ diff --git a/images_v1/mirror_moins.png b/images_v1/mirror_moins.png new file mode 100644 index 0000000..a0cb7e9 Binary files /dev/null and b/images_v1/mirror_moins.png differ diff --git a/images_v1/mirror_plus.png b/images_v1/mirror_plus.png new file mode 100644 index 0000000..04362b6 Binary files /dev/null and b/images_v1/mirror_plus.png differ diff --git a/images_v1/mirror_pulse.png b/images_v1/mirror_pulse.png new file mode 100644 index 0000000..1781f8f Binary files /dev/null and b/images_v1/mirror_pulse.png differ diff --git a/images_v1/mirror_pulse.svg b/images_v1/mirror_pulse.svg new file mode 100644 index 0000000..f723a3f --- /dev/null +++ b/images_v1/mirror_pulse.svg @@ -0,0 +1,18 @@ + + + + + + Pulsé + + + +24 + + + + + + + + + diff --git a/images_v1/mirror_relais_cote.png b/images_v1/mirror_relais_cote.png new file mode 100644 index 0000000..570dde5 Binary files /dev/null and b/images_v1/mirror_relais_cote.png differ diff --git a/images_v1/mirror_repos_ferme.png b/images_v1/mirror_repos_ferme.png new file mode 100644 index 0000000..2156b23 Binary files /dev/null and b/images_v1/mirror_repos_ferme.png differ diff --git a/images_v1/mirror_repos_ouvert.png b/images_v1/mirror_repos_ouvert.png new file mode 100644 index 0000000..818f826 Binary files /dev/null and b/images_v1/mirror_repos_ouvert.png differ diff --git a/images_v1/mirror_travail_ferme.png b/images_v1/mirror_travail_ferme.png new file mode 100644 index 0000000..06cf109 Binary files /dev/null and b/images_v1/mirror_travail_ferme.png differ diff --git a/images_v1/mirror_travail_ouvert.png b/images_v1/mirror_travail_ouvert.png new file mode 100644 index 0000000..5b06302 Binary files /dev/null and b/images_v1/mirror_travail_ouvert.png differ diff --git a/images_v1/moins.png b/images_v1/moins.png new file mode 100644 index 0000000..fd0f1bb Binary files /dev/null and b/images_v1/moins.png differ diff --git a/images_v1/moins.svg b/images_v1/moins.svg new file mode 100644 index 0000000..500971a --- /dev/null +++ b/images_v1/moins.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + - + + + diff --git a/images_v1/noeud.png b/images_v1/noeud.png new file mode 100644 index 0000000..2c939bd Binary files /dev/null and b/images_v1/noeud.png differ diff --git a/images_v1/noeud.svg b/images_v1/noeud.svg new file mode 100644 index 0000000..1baee5e --- /dev/null +++ b/images_v1/noeud.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/images_v1/plus.png b/images_v1/plus.png new file mode 100644 index 0000000..9e74a22 Binary files /dev/null and b/images_v1/plus.png differ diff --git a/images_v1/plus.svg b/images_v1/plus.svg new file mode 100644 index 0000000..51e40ae --- /dev/null +++ b/images_v1/plus.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + +24 + + + diff --git a/images_v1/pulse.png b/images_v1/pulse.png new file mode 100644 index 0000000..7cd3201 Binary files /dev/null and b/images_v1/pulse.png differ diff --git a/images_v1/pulse.svg b/images_v1/pulse.svg new file mode 100644 index 0000000..e883c82 --- /dev/null +++ b/images_v1/pulse.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + +24 + + + Pulsé + + + diff --git a/images_v1/relais.png b/images_v1/relais.png new file mode 100644 index 0000000..6c0387b Binary files /dev/null and b/images_v1/relais.png differ diff --git a/images_v1/relais.svg b/images_v1/relais.svg new file mode 100644 index 0000000..5fb0bed --- /dev/null +++ b/images_v1/relais.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/relais_basculeur.png b/images_v1/relais_basculeur.png new file mode 100644 index 0000000..50f58a2 Binary files /dev/null and b/images_v1/relais_basculeur.png differ diff --git a/images_v1/relais_basculeur.svg b/images_v1/relais_basculeur.svg new file mode 100644 index 0000000..9c57cd3 --- /dev/null +++ b/images_v1/relais_basculeur.svg @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/relais_cote.png b/images_v1/relais_cote.png new file mode 100644 index 0000000..1236fb7 Binary files /dev/null and b/images_v1/relais_cote.png differ diff --git a/images_v1/relais_cote.svg b/images_v1/relais_cote.svg new file mode 100644 index 0000000..3a59aac --- /dev/null +++ b/images_v1/relais_cote.svg @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/repos_ferme.png b/images_v1/repos_ferme.png new file mode 100644 index 0000000..948fc40 Binary files /dev/null and b/images_v1/repos_ferme.png differ diff --git a/images_v1/repos_ferme.svg b/images_v1/repos_ferme.svg new file mode 100644 index 0000000..e62178f --- /dev/null +++ b/images_v1/repos_ferme.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/repos_ouvert.png b/images_v1/repos_ouvert.png new file mode 100644 index 0000000..9bb1146 Binary files /dev/null and b/images_v1/repos_ouvert.png differ diff --git a/images_v1/repos_ouvert.svg b/images_v1/repos_ouvert.svg new file mode 100644 index 0000000..cedde6c --- /dev/null +++ b/images_v1/repos_ouvert.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/resistance.png b/images_v1/resistance.png new file mode 100644 index 0000000..d1d5911 Binary files /dev/null and b/images_v1/resistance.png differ diff --git a/images_v1/resistance.svg b/images_v1/resistance.svg new file mode 100644 index 0000000..90c7feb --- /dev/null +++ b/images_v1/resistance.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/tempo_ferme.png b/images_v1/tempo_ferme.png new file mode 100644 index 0000000..bc2eb50 Binary files /dev/null and b/images_v1/tempo_ferme.png differ diff --git a/images_v1/tempo_ferme.svg b/images_v1/tempo_ferme.svg new file mode 100644 index 0000000..629aaa0 --- /dev/null +++ b/images_v1/tempo_ferme.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/tempo_ouvert.png b/images_v1/tempo_ouvert.png new file mode 100644 index 0000000..9a6a813 Binary files /dev/null and b/images_v1/tempo_ouvert.png differ diff --git a/images_v1/tempo_ouvert.svg b/images_v1/tempo_ouvert.svg new file mode 100644 index 0000000..80d366a --- /dev/null +++ b/images_v1/tempo_ouvert.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/images_v1/travail_ferme.png b/images_v1/travail_ferme.png new file mode 100644 index 0000000..b3b9ba6 Binary files /dev/null and b/images_v1/travail_ferme.png differ diff --git a/images_v1/travail_ferme.svg b/images_v1/travail_ferme.svg new file mode 100644 index 0000000..0e35948 --- /dev/null +++ b/images_v1/travail_ferme.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/images_v1/travail_ouvert.png b/images_v1/travail_ouvert.png new file mode 100644 index 0000000..7d54df4 Binary files /dev/null and b/images_v1/travail_ouvert.png differ diff --git a/images_v1/travail_ouvert.svg b/images_v1/travail_ouvert.svg new file mode 100644 index 0000000..fe4372a --- /dev/null +++ b/images_v1/travail_ouvert.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + diff --git a/modele/__pycache__/cocosim.cpython-312.pyc b/modele/__pycache__/cocosim.cpython-312.pyc new file mode 100644 index 0000000..eb8bd23 Binary files /dev/null and b/modele/__pycache__/cocosim.cpython-312.pyc differ diff --git a/modele/__pycache__/composants.cpython-312.pyc b/modele/__pycache__/composants.cpython-312.pyc new file mode 100644 index 0000000..a0ae861 Binary files /dev/null and b/modele/__pycache__/composants.cpython-312.pyc differ diff --git a/modele/__pycache__/donnees.cpython-312.pyc b/modele/__pycache__/donnees.cpython-312.pyc new file mode 100644 index 0000000..1e61176 Binary files /dev/null and b/modele/__pycache__/donnees.cpython-312.pyc differ diff --git a/modele/__pycache__/elements.cpython-312.pyc b/modele/__pycache__/elements.cpython-312.pyc new file mode 100644 index 0000000..e85419f Binary files /dev/null and b/modele/__pycache__/elements.cpython-312.pyc differ diff --git a/modele/__pycache__/exceptions.cpython-312.pyc b/modele/__pycache__/exceptions.cpython-312.pyc new file mode 100644 index 0000000..d1d5cab Binary files /dev/null and b/modele/__pycache__/exceptions.cpython-312.pyc differ diff --git a/modele/__pycache__/graphe.cpython-312.pyc b/modele/__pycache__/graphe.cpython-312.pyc new file mode 100644 index 0000000..6730ddb Binary files /dev/null and b/modele/__pycache__/graphe.cpython-312.pyc differ diff --git a/modele/__pycache__/matrice.cpython-312.pyc b/modele/__pycache__/matrice.cpython-312.pyc new file mode 100644 index 0000000..e3238fa Binary files /dev/null and b/modele/__pycache__/matrice.cpython-312.pyc differ diff --git a/modele/cocosim.py b/modele/cocosim.py new file mode 100644 index 0000000..46d3ad7 --- /dev/null +++ b/modele/cocosim.py @@ -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() + + diff --git a/modele/composants.py b/modele/composants.py new file mode 100644 index 0000000..9382e80 --- /dev/null +++ b/modele/composants.py @@ -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 diff --git a/modele/config.json b/modele/config.json new file mode 100644 index 0000000..cdc17c8 --- /dev/null +++ b/modele/config.json @@ -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 + } + } + \ No newline at end of file diff --git a/modele/donnees.py b/modele/donnees.py new file mode 100644 index 0000000..4336fbb --- /dev/null +++ b/modele/donnees.py @@ -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"" + + 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 \ No newline at end of file diff --git a/modele/elements.py b/modele/elements.py new file mode 100644 index 0000000..c03400e --- /dev/null +++ b/modele/elements.py @@ -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) \ No newline at end of file diff --git a/modele/exceptions.py b/modele/exceptions.py new file mode 100644 index 0000000..573d57a --- /dev/null +++ b/modele/exceptions.py @@ -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 \ No newline at end of file diff --git a/modele/graphe.py b/modele/graphe.py new file mode 100644 index 0000000..574c49d --- /dev/null +++ b/modele/graphe.py @@ -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()] diff --git a/modele/matrice.py b/modele/matrice.py new file mode 100644 index 0000000..30b3bf8 --- /dev/null +++ b/modele/matrice.py @@ -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) diff --git a/requirements.txt b/requirements.txt index e69de29..4755eea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,3 @@ +numpy>=1.24.3 +flask>=2.3.2 +Flask-Cors>=4.0.0 \ No newline at end of file diff --git a/run.py b/run.py index e69de29..e828d5d 100644 --- a/run.py +++ b/run.py @@ -0,0 +1,6 @@ +from api_server.app import main + +import sys + +if __name__ == "__main__": + main(sys.argv) diff --git a/schemas_v1/basculeur.ccs b/schemas_v1/basculeur.ccs new file mode 100644 index 0000000..adaf496 --- /dev/null +++ b/schemas_v1/basculeur.ccs @@ -0,0 +1 @@ +{"blocs":{"p24-1":{"type":"P24","valeur":24,"x":0,"y":526},"b1":{"type":"Bouton","x":100,"y":526,"orientation":0},"rbas":{"type":"RelaisBasculeur","x":200,"y":520,"orientation":0},"b2":{"type":"Bouton","x":350,"y":526,"orientation":180},"p24-2":{"type":"P24","valeur":24,"x":450,"y":526,"orientation":180},"p0-1":{"type":"P0","x":0,"y":512,"orientation":180},"p0-2":{"type":"P0","x":450,"y":512,"orientation":0},"p0-3":{"type":"P0","x":0,"y":299,"orientation":180},"p0-4":{"type":"P0","x":450,"y":299,"orientation":0},"l3":{"type":"Lampe","x":100,"y":299,"orientation":0},"l4":{"type":"Lampe","x":350,"y":299,"orientation":0},"rbas.basc":{"type":"ContactBasculeur","relais":"rbas","x":200,"y":285,"orientation":0},"p24-3":{"type":"P24","x":150,"y":258.2}},"cables":[["p24-1","d","b1","g"],["b1","d","rbas","hg"],["p0-1","d","rbas","bg"],["p24-2","g","b2","d"],["b2","g","rbas","hd"],["p0-2","g","rbas","bd"],["p0-3","d","l3","g"],["p0-4","g","l4","d"],["l3","d","rbas.basc","g"],["l4","g","rbas.basc","d"],["p24-3","d","rbas.basc","b"]]} \ No newline at end of file diff --git a/schemas_v1/condensateur.ccs b/schemas_v1/condensateur.ccs new file mode 100644 index 0000000..b446539 --- /dev/null +++ b/schemas_v1/condensateur.ccs @@ -0,0 +1,104 @@ +{ + "blocs": { + "p24-1": { + "type": "P24", + "valeur": 24.0, + "x": 0, + "y": 500 + }, + "p0-1": { + "type": "P0", + "valeur": 0.0, + "x": 1200, + "y": 500 + }, + "p0-2": { + "type": "P0", + "valeur": 0.0, + "x": 1200, + "y": 300 + }, + "p0-3": { + "type": "P0", + "valeur": 0.0, + "x": 600, + "y": 300 + }, + "xx": { + "type": "Noeud", + "x": 300, + "y": 500 + }, + "yy": { + "type": "Coude", + "x": 300, + "y": 300 + }, + "k1": { + "type": "Condensateur", + "capacite": 0.4, + "coef_mul_tension": 24, + "x": 400, + "y": 500 + }, + "r2": { + "type": "Relais", + "x": 1000, + "y": 500 + }, + "c3": { + "type": "Bouton", + "x": 200, + "y": 500, + "orientation": 180 + }, + "r2.travail": { + "type": "ContactTravail", + "relais": "r2", + "x": 1000, + "y": 300 + }, + "w1": { + "type": "Lampe", + "x": 600, + "y": 500 + }, + "n1": { + "type": "Noeud", + "x": 800, + "y": 500 + }, + "coude1": { + "type": "Coude", + "x": 800, + "y": 300 + }, + "b2": { + "type": "Bouton", + "x": 400, + "y": 300 + }, + "w2": { + "type": "Resistance", + "valeur": 20, + "x": 500, + "y": 300 + } + }, + "cables": [ + ["p24-1", "d", "c3", "g"], + ["c3", "d", "xx", "m"], + ["xx", "m", "k1", "g"], + ["k1", "d", "w1", "g"], + ["w1", "d", "n1", "m"], + ["n1", "m", "r2", "g"], + ["r2", "d", "p0-1", "g"], + ["n1", "m", "coude1", "m"], + ["coude1", "m", "r2.travail", "g"], + ["r2.travail", "d", "p0-2", "g"], + ["xx", "m", "yy", "m"], + ["yy", "m", "b2", "g"], + ["b2", "d", "w2", "g"], + ["w2", "d", "p0-3", "g"] + ] +} \ No newline at end of file diff --git a/schemas_v1/contact_double.ccs b/schemas_v1/contact_double.ccs new file mode 100644 index 0000000..d04d979 --- /dev/null +++ b/schemas_v1/contact_double.ccs @@ -0,0 +1,87 @@ +{ + "blocs": { + "p24-1": { + "type": "P24", + "valeur": 24.0, + "orientation": 0, + "x": 0.0, + "y": 500.0 + }, + "p24-2": { + "type": "P24", + "valeur": 24.0, + "orientation": 0, + "x": 0.0, + "y": 300.0 + }, + "p0-1": { + "type": "P0", + "valeur": 0.0, + "orientation": 0, + "x": 1200.0, + "y": 500.0 + }, + "p0-2": { + "type": "P0", + "valeur": 0.0, + "x": 1200.0, + "y": 300.0 + }, + "r1": { + "type": "Relais", + "x": 400.0, + "y": 500.0 + }, + "r2": { + "type": "Relais", + "x": 1000.0, + "y": 500.0 + }, + "c3": { + "type": "Bouton", + "x": 200.0, + "y": 500.0, + "orientation": 0 + }, + "r2.double": { + "type": "ContactDouble", + "relais": "r2", + "orientation": 0, + "x": 1000.0, + "y": 300.0 + }, + "w1": { + "type": "Lampe", + "x": 600.0, + "y": 500.0 + }, + "w2": { + "type": "Lampe", + "x": 600.0, + "y": 300.0 + }, + "n1": { + "type": "Noeud", + "x": 800.0, + "y": 500 + }, + "coude1": { + "type": "Coude", + "x": 800, + "y": 310 + } + }, + "cables": [ + ["p24-1", "d", "c3", "g"], + ["c3", "d", "r1", "g"], + ["r1", "d", "w1", "g"], + ["w1", "d", "n1", "m"], + ["n1", "m", "r2", "g"], + ["r2", "d", "p0-1", "g"], + ["n1", "m", "coude1", "m"], + ["coude1", "m", "r2.double", "hg"], + ["r2.double", "d", "p0-2", "g"], + ["p24-2", "d", "w2", "g"], + ["w2", "d", "r2.double", "bg"] + ] +} \ No newline at end of file diff --git a/schemas_v1/contact_double_180.ccs b/schemas_v1/contact_double_180.ccs new file mode 100644 index 0000000..efb84d0 --- /dev/null +++ b/schemas_v1/contact_double_180.ccs @@ -0,0 +1,73 @@ +{ + "blocs": { + "p24": { + "type": "P24", + "valeur": 24.0, + "x": 0, + "y": 500 + }, + "b": { + "type": "Levier", + "x": 100, + "y": 500 + }, + "r": { + "type": "Relais", + "x": 200, + "y": 500 + }, + "p0-1": { + "type": "P0", + "x": 400, + "y": 500 + }, + "pulse": { + "type": "Pulse", + "valeur": 24.0, + "periode": 0.5, + "x": 0, + "y": 300 + }, + "r.double": { + "type": "ContactDouble", + "relais": "r", + "orientation": 180, + "x": 200, + "y": 300 + }, + "l1": { + "type": "Lampe", + "x": 300, + "y": 330 + }, + "l2": { + "type": "Lampe", + "x": 300, + "y": 270 + }, + "p0-2": { + "type": "P0", + "valeur": 0.0, + "x": 400, + "y": 330 + }, + "p0-3": { + "type": "P0", + "valeur": 0.0, + "x": 400, + "y": 270 + } + }, + "cables": [ + ["p24", "d", "b", "g"], + ["b", "d", "r", "g"], + ["r", "d", "p0-1", "g"], + + ["pulse", "d", "r.double", "g"], + ["r.double", "hd", "l1", "g"], + ["l1", "d", "p0-2", "g"], + + ["r.double", "bd", "l2", "g"], + ["l2", "d", "p0-3", "g"] + ] +} diff --git a/schemas_v1/diodes.ccs b/schemas_v1/diodes.ccs new file mode 100644 index 0000000..6d96bbe --- /dev/null +++ b/schemas_v1/diodes.ccs @@ -0,0 +1,68 @@ +{ + "blocs": { + "pulse": { + "type": "Pulse", + "valeur": 24.0, + "periode": 1, + "x": 0, + "y": 500 + }, + "noeud": { + "type": "Noeud", + "x": 200, + "y": 500 + }, + "coude2": { + "type": "Coude", + "x": 200, + "y": 300 + }, + + "diode1": { + "type": "Diode", + "orientation": 0, + "x": 350, + "y": 500 + }, + "diode2": { + "type": "Diode", + "orientation": 180, + "x": 350, + "y": 300 + }, + + "p0-1": { + "type": "P0", + "valeur": 0.0, + "x": 600, + "y": 500 + }, + "p0-2": { + "type": "P0", + "valeur": 0.0, + "x": 600, + "y": 300 + }, + "l1": { + "type": "Lampe", + "x": 450, + "y": 500 + }, + "l2": { + "type": "Lampe", + "x": 450, + "y": 300 + } + + }, + "cables": [ + ["pulse", "d", "noeud", "m"], + ["noeud", "m", "diode1", "g"], + ["diode1", "d", "l1", "g"], + ["l1", "d", "p0-1", "g"], + ["noeud", "m", "coude2", "m"], + ["coude2", "m", "diode2", "g"], + ["diode2", "d", "l2", "g"], + ["l2", "d", "p0-2", "g"] + ] +} diff --git a/schemas_v1/errors/error.ccs b/schemas_v1/errors/error.ccs new file mode 100644 index 0000000..711fad8 --- /dev/null +++ b/schemas_v1/errors/error.ccs @@ -0,0 +1,70 @@ +{ + "blocs": + "p24-1": { + "type": "P24", + "valeur": 24.0, + "x": 0, + "y": 500 + }, + "p0-1": { + "type": "P0", + "valeur": 0.0, + "x": 1200, + "y": 500 + }, + "p0-2": { + "type": "P0", + "valeur": 0.0, + "x": 1200, + "y": 300 + }, + "r1": { + "type": "Relais", + "x": 400, + "y": 500 + }, + "r2": { + "type": "Relais", + "x": 1000, + "y": 500 + }, + "c3": { + "type": "Bouton", + "x": 200, + "y": 500 + }, + "r2.travail": { + "type": "ContactTravail", + "x": 1000, + "y": 300 + }, + "t1": { + "type": "Tempo", + "tempo_blocage": 2, + "tempo_liberation": 1, + "x": 600, + "y": 500 + }, + "n1": { + "type": "Noeud", + "x": 800, + "y": 500 + }, + "coude1": { + "type": "Coude", + "x": 800, + "y": 300 + } + }, + "cables": [ + ["p24-1", "d", "c3", "g"], + ["c3", "d", "r1", "g"], + ["r1", "d", "t1", "g"], + ["t1", "d", "n1", "m"], + ["n1", "m", "r2", "g"], + ["r2", "d", "p0-1", "g"], + ["n1", "m", "coude1", "m"], + ["coude1", "m", "r2.travail", "g"], + ["r2.travail", "d", "p0-2", "g"] + ] +} diff --git a/schemas_v1/errors/error_id.ccs b/schemas_v1/errors/error_id.ccs new file mode 100644 index 0000000..77c108e --- /dev/null +++ b/schemas_v1/errors/error_id.ccs @@ -0,0 +1,72 @@ +{ + "blocs": { + "p24-1": { + "type": "P24", + "valeur": 24.0, + "orientation": 0, + "x": 0.0, + "y": 500.0 + }, + "p0-1": { + "type": "P0", + "valeur": 0.0, + "orientation": 0, + "x": 1200.0, + "y": 500.0 + }, + "p0-2": { + "type": "P0", + "valeur": 0.0, + "x": 1200.0, + "y": 300.0 + }, + "r1": { + "type": "Relais", + "x": 400.0, + "y": 500.0 + }, + "r2": { + "type": "Relais", + "x": 1000.0, + "y": 500.0 + }, + "c3": { + "type": "Bouton", + "x": 200.0, + "y": 500.0, + "orientation": 0 + }, + "r2.travail": { + "type": "ContactTravail", + "orientation": 0, + "x": 1000.0, + "y": 300.0 + }, + "w1": { + "type": "Lampe", + "x": 600.0, + "y": 500.0 + }, + "n1": { + "type": "Noeud", + "x": 800.0, + "y": 500 + }, + "coude1": { + "type": "Coude", + "x": 800, + "y": 300 + } + }, + "cables": [ + ["p24-1", "d", "c3", "g"], + ["c3", "d", "r1", "g"], + ["r1", "d", "w1", "g"], + ["w1", "d", "n2", "m"], + ["n1", "m", "r2", "g"], + ["r2", "d", "p0-1", "g"], + ["n1", "m", "coude1", "m"], + ["coude1", "m", "r2.travail", "g"], + ["r2.travail", "d", "p0-2", "g"] + ] +} \ No newline at end of file diff --git a/schemas_v1/errors/error_short_cc.ccs b/schemas_v1/errors/error_short_cc.ccs new file mode 100644 index 0000000..23be686 --- /dev/null +++ b/schemas_v1/errors/error_short_cc.ccs @@ -0,0 +1,20 @@ +{ + "blocs": { + "pulse": { + "type": "Pulse", + "valeur": 24.0, + "periode": 0.5, + "x": 0, + "y": 500 + }, + "p0-1": { + "type": "P0", + "valeur": 0.0, + "x": 400, + "y": 500 + } + }, + "cables": [ + ["pulse", "d", "p0-1", "g"] + ] +} diff --git a/schemas_v1/errors/no_pot.ccs b/schemas_v1/errors/no_pot.ccs new file mode 100644 index 0000000..65069a5 --- /dev/null +++ b/schemas_v1/errors/no_pot.ccs @@ -0,0 +1,24 @@ +{ + "blocs": { + "n1": { + "type": "Noeud", + "x": 0, + "y": 500 + }, + "n2": { + "type": "Noeud", + "valeur": 0.0, + "x": 400, + "y": 500 + }, + "l": { + "type": "Lampe", + "x": 250, + "y": 500 + } + }, + "cables": [ + ["n1", "m", "l", "g"], + ["l", "d", "n2", "m"] + ] +} diff --git a/schemas_v1/errors/one_pot.ccs b/schemas_v1/errors/one_pot.ccs new file mode 100644 index 0000000..5209f76 --- /dev/null +++ b/schemas_v1/errors/one_pot.ccs @@ -0,0 +1,26 @@ +{ + "blocs": { + "pulse": { + "type": "Pulse", + "valeur": 24.0, + "periode": 1.0, + "x": 0, + "y": 500 + }, + "n": { + "type": "Noeud", + "valeur": 0.0, + "x": 400, + "y": 500 + }, + "l": { + "type": "Lampe", + "x": 250, + "y": 500 + } + }, + "cables": [ + ["pulse", "d", "l", "g"], + ["l", "d", "n", "m"] + ] +} diff --git a/schemas_v1/pulse.ccs b/schemas_v1/pulse.ccs new file mode 100644 index 0000000..df06e0d --- /dev/null +++ b/schemas_v1/pulse.ccs @@ -0,0 +1,26 @@ +{ + "blocs": { + "pulse": { + "type": "Pulse", + "valeur": 24.0, + "periode": 1.0, + "x": 0, + "y": 500 + }, + "p0-1": { + "type": "P0", + "valeur": 0.0, + "x": 400, + "y": 500 + }, + "l": { + "type": "Lampe", + "x": 250, + "y": 500 + } + }, + "cables": [ + ["pulse", "d", "l", "g"], + ["l", "d", "p0-1", "g"] + ] +} diff --git a/schemas_v1/relais_cote.ccs b/schemas_v1/relais_cote.ccs new file mode 100644 index 0000000..da59ccc --- /dev/null +++ b/schemas_v1/relais_cote.ccs @@ -0,0 +1,75 @@ +{ + "blocs": { + "p24": { + "type": "P24", + "valeur": 24.0, + "x": 0, + "y": 550 + }, + "p0": { + "type": "P0", + "valeur": 0.0, + "orientation": 180, + "x": 0, + "y": 470 + }, + "b": { + "type": "Bouton", + "x": 120, + "y": 550 + }, + "c1": { + "type": "Coude", + "x": 200, + "y": 550 + }, + "c2": { + "type": "Coude", + "x": 200, + "y": 485 + }, + "r": { + "type": "RelaisCote", + "x": 375, + "y": 475, + "orientation": 0 + }, + + + "pulse": { + "type": "Pulse", + "valeur": 24.0, + "periode": 1, + "x": 0, + "y": 400 + }, + "p0-1": { + "type": "P0", + "valeur": 0.0, + "x": 475, + "y": 400 + }, + "r.repos": { + "type": "ContactRepos", + "x": 375, + "y": 400, + "relais": "r" + }, + "l": { + "type": "Lampe", + "x": 250, + "y": 400 + } + }, + "cables": [ + ["p24", "d", "b", "g"], + ["b", "d", "c1", "m"], + ["c1", "m", "c2", "m"], + ["c2", "m", "r", "hg"], + ["p0", "d", "r", "bg"], + + ["pulse", "d", "l", "g"], + ["l", "d", "r.repos", "g"], + ["r.repos", "d", "p0-1", "g"] + ] +} diff --git a/schemas_v1/schema_s4.ccs b/schemas_v1/schema_s4.ccs new file mode 100644 index 0000000..38f826c --- /dev/null +++ b/schemas_v1/schema_s4.ccs @@ -0,0 +1,73 @@ +{ + "blocs": { + "p24-1": { + "type": "P24", + "valeur": 24.0, + "orientation": 0, + "x": 0.0, + "y": 500.0 + }, + "p0-1": { + "type": "P0", + "valeur": 0.0, + "orientation": 0, + "x": 1200.0, + "y": 500.0 + }, + "p0-2": { + "type": "P0", + "valeur": 0.0, + "x": 1200.0, + "y": 300.0 + }, + "r1": { + "type": "Relais", + "x": 400.0, + "y": 500.0 + }, + "r2": { + "type": "Relais", + "x": 1000.0, + "y": 500.0 + }, + "c3": { + "type": "Bouton", + "x": 200.0, + "y": 500.0, + "orientation": 0 + }, + "r2.travail": { + "type": "ContactTravail", + "orientation": 0, + "relais": "r2", + "x": 1000.0, + "y": 300.0 + }, + "w1": { + "type": "Lampe", + "x": 600.0, + "y": 500.0 + }, + "n1": { + "type": "Noeud", + "x": 800.0, + "y": 500 + }, + "coude1": { + "type": "Coude", + "x": 800, + "y": 300 + } + }, + "cables": [ + ["p24-1", "d", "c3", "g"], + ["c3", "d", "r1", "g"], + ["r1", "d", "w1", "g"], + ["w1", "d", "n1", "m"], + ["n1", "m", "r2", "g"], + ["r2", "d", "p0-1", "g"], + ["n1", "m", "coude1", "m"], + ["coude1", "m", "r2.travail", "g"], + ["r2.travail", "d", "p0-2", "g"] + ] +} \ No newline at end of file diff --git a/schemas_v1/simple.ccs b/schemas_v1/simple.ccs new file mode 100644 index 0000000..66c0208 --- /dev/null +++ b/schemas_v1/simple.ccs @@ -0,0 +1,71 @@ +{ + "blocs": { + "p24-1": { + "type": "P24", + "valeur": 24.0, + "x": 0, + "y": 500 + }, + "p0-1": { + "type": "P0", + "valeur": 0.0, + "x": 1200, + "y": 500 + }, + "p0-2": { + "type": "P0", + "valeur": 0.0, + "x": 1200, + "y": 300 + }, + "r1": { + "type": "Relais", + "x": 400, + "y": 500 + }, + "r2": { + "type": "Relais", + "x": 1000, + "y": 500 + }, + "c3": { + "type": "Bouton", + "x": 200, + "y": 500 + }, + "r2.travail": { + "type": "ContactTravail", + "x": 1000, + "y": 300, + "relais": "r2" + }, + "t1": { + "type": "Tempo", + "tempo_blocage": 2, + "tempo_liberation": 1, + "x": 600, + "y": 500 + }, + "n1": { + "type": "Noeud", + "x": 800, + "y": 500 + }, + "coude1": { + "type": "Coude", + "x": 800, + "y": 300 + } + }, + "cables": [ + ["p24-1", "d", "c3", "g"], + ["c3", "d", "r1", "g"], + ["r1", "d", "t1", "g"], + ["t1", "d", "n1", "m"], + ["n1", "m", "r2", "g"], + ["r2", "d", "p0-1", "g"], + ["n1", "m", "coude1", "m"], + ["coude1", "m", "r2.travail", "g"], + ["r2.travail", "d", "p0-2", "g"] + ] +}