risansym 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
risansym/__init__.py ADDED
@@ -0,0 +1 @@
1
+
risansym/event.py ADDED
@@ -0,0 +1,14 @@
1
+ from dataclasses import dataclass, field
2
+
3
+
4
+ @dataclass(order=True, slots=True)
5
+ class Event:
6
+ """Encapsula la información que se intercambia entre procesos activos."""
7
+
8
+ time: float
9
+ # field(compare=False) evita desempates por nombre/IDs si los tiempos son iguales
10
+ name: str = field(compare=False)
11
+ target: int = field(compare=False)
12
+ source: int = field(compare=False)
13
+ payload: dict = field(default_factory=dict, compare=False)
14
+
risansym/model.py ADDED
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING
5
+
6
+ from risansym.event import Event
7
+
8
+ if TYPE_CHECKING:
9
+ from risansym.process import Process
10
+
11
+
12
+ class Model(ABC):
13
+ """Interfaz abstracta (contrato) para los algoritmos distribuidos."""
14
+
15
+ def __init__(self) -> None:
16
+ self.clock: float = 0.0
17
+ self.process: Process | None = None
18
+ self.neighbors: list[int] = []
19
+ self.id: int = 0
20
+
21
+ def set_time(self, time: float) -> None:
22
+ self.clock = time
23
+
24
+ def set_process(self, process: Process, neighbors: list[int], node_id: int) -> None:
25
+ self.process = process
26
+ self.neighbors = neighbors
27
+ self.id = node_id
28
+
29
+ def transmit(self, event: Event) -> None:
30
+ if self.process:
31
+ self.process.transmit(event)
32
+
33
+ def log(self, message: str) -> None:
34
+ """Convención: Usar este método en lugar de print() para reportar eventos lógicos de la capa de aplicación."""
35
+ if self.process:
36
+ self.process.log(message)
37
+
38
+ @abstractmethod
39
+ def init(self) -> None:
40
+ """Inicialización del estado local (implementado por la subclase)."""
41
+
42
+ @abstractmethod
43
+ def receive(self, event: Event) -> None:
44
+ """Lógica de transición de la máquina de estados (implementado por subclase)."""
risansym/process.py ADDED
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from risansym.event import Event
4
+ from risansym.model import Model
5
+ from risansym.simulator import Simulator
6
+
7
+
8
+ class Process:
9
+ """Entidad que reside en un vértice de la topología."""
10
+
11
+ def __init__(self, neighbors: list[int], engine: Simulator, node_id: int) -> None:
12
+ self.neighbors = neighbors
13
+ self.engine = engine
14
+ self.id = node_id
15
+ self.model: Model | None = None
16
+
17
+ def set_model(self, model: Model) -> None:
18
+ self.model = model
19
+ self.model.set_process(self, self.neighbors, self.id)
20
+ self.model.init()
21
+
22
+ def set_time(self, time: float) -> None:
23
+ if self.model:
24
+ self.model.set_time(time)
25
+
26
+ def transmit(self, event: Event) -> None:
27
+ self.engine.insert_event(event)
28
+
29
+ def receive(self, event: Event) -> None:
30
+ if self.model:
31
+ self.model.receive(event)
32
+
33
+ def log(self, message: str) -> None:
34
+ self.engine.log_app_event(self.id, message)
risansym/schemas.py ADDED
@@ -0,0 +1,41 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Any, Literal, Union
3
+
4
+ class TransmitEvent(BaseModel):
5
+ action: Literal["TRANSMIT"] = "TRANSMIT"
6
+ clock: float = Field(..., description="Tiempo en el que el emisor disparó el evento")
7
+ event_time: float = Field(..., description="Tiempo calculado de llegada al destino")
8
+ source: int
9
+ target: int
10
+ name: str
11
+ payload: Any
12
+
13
+ class ReceiveEvent(BaseModel):
14
+ action: Literal["RECEIVE"] = "RECEIVE"
15
+ clock: float = Field(..., description="Tiempo en el que el nodo procesa el evento")
16
+ source: int
17
+ target: int
18
+ name: str
19
+ payload: Any
20
+
21
+ class AppLogEvent(BaseModel):
22
+ action: Literal["APP_LOG"] = "APP_LOG"
23
+ clock: float
24
+ source: int
25
+ message: str
26
+
27
+ # Agrupación de todos los eventos válidos
28
+ TraceEvent = Union[TransmitEvent, ReceiveEvent, AppLogEvent]
29
+
30
+ class TraceMetadata(BaseModel):
31
+ schema_version: Literal["1.0"] = "1.0"
32
+ algorithm: str
33
+ topology: str
34
+ tag: str | None = None
35
+ execution_date: str
36
+ parameters: dict[str, Any]
37
+ metrics: dict[str, Any]
38
+
39
+ class TraceOutput(BaseModel):
40
+ metadata: TraceMetadata
41
+ trace: list[TraceEvent]
risansym/simulation.py ADDED
@@ -0,0 +1,104 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ import time
5
+ from pathlib import Path
6
+
7
+ from risansym.event import Event
8
+ from risansym.model import Model
9
+ from risansym.process import Process
10
+ from risansym.simulator import Simulator
11
+ from risansym.schemas import TraceMetadata
12
+
13
+
14
+ class Simulation:
15
+ """Orquestador global del grafo computacional y ciclo de simulación."""
16
+
17
+ def __init__(self, filename: str | Path, maxtime: float, algo_name: str = "UnknownAlgo", debug: bool = True, trace: bool | str | Path = False, trace_dir: str = "traces", trace_tag: str | None = None) -> None:
18
+ from risansym.trace import TraceCollector
19
+
20
+ self.filename = Path(filename)
21
+ self.algo_name = algo_name
22
+ self.trace = trace
23
+ self.trace_dir = trace_dir
24
+ self.trace_tag = trace_tag
25
+
26
+ self.collector = TraceCollector() if trace else None
27
+ self.engine = Simulator(maxtime, debug=debug, collector=self.collector)
28
+ self.graph = self._load_adjacency_matrix(self.filename)
29
+ # Índice 0 reservado como None; nodos indexados desde 1
30
+ self.table: list[Process | None] = [None] + [
31
+ Process(row, self.engine, i)
32
+ for i, row in enumerate(self.graph, start=1)
33
+ ]
34
+
35
+ def _load_adjacency_matrix(self, filename: str | Path) -> list[list[int]]:
36
+ """Construye la topología G=(V,E) desde archivo."""
37
+ return [
38
+ [int(node) for node in line.split()]
39
+ for line in Path(filename).read_text().splitlines()
40
+ if line.strip()
41
+ ]
42
+
43
+ def set_model(self, model: Model, node_id: int) -> None:
44
+ if process := self.table[node_id]: # walrus operator: asigna y evalúa en una línea
45
+ process.set_model(model)
46
+
47
+ def init(self, event: Event) -> None:
48
+ self.engine.insert_event(event)
49
+
50
+ def _execute(self) -> None:
51
+ """Bucle principal: extrae y enruta eventos hasta agotar la agenda."""
52
+ start_real_time = time.perf_counter()
53
+
54
+ while self.engine.is_on:
55
+ event = self.engine.pop_event()
56
+ if process := self.table[event.target]: # walrus operator
57
+ process.set_time(event.time)
58
+ process.receive(event)
59
+
60
+ end_real_time = time.perf_counter()
61
+
62
+ self.execution_metrics = {
63
+ "simulated_time_elapsed": self.engine.clock,
64
+ "total_messages": self.collector.get_event_count() if self.collector else 0,
65
+ "execution_real_time_sec": round(end_real_time - start_real_time, 5)
66
+ }
67
+
68
+ def _save_trace(self) -> None:
69
+ """Serializa y persiste la traza de eventos simulados con sus metadatos."""
70
+ grafo_name = self.filename.stem
71
+ tag = f"_{self.trace_tag}" if self.trace_tag else ""
72
+ timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
73
+
74
+ if isinstance(self.trace, (str, Path)) and not isinstance(self.trace, bool):
75
+ trace_path = Path(self.trace)
76
+ else:
77
+ file_name = f"{self.algo_name}_{grafo_name}{tag}_{timestamp}.json"
78
+ trace_path = Path(self.trace_dir) / self.algo_name / file_name
79
+
80
+ total_edges = sum(len(neighbors) for neighbors in self.graph)
81
+ total_nodes = len(self.table) - 1
82
+
83
+ metadata = TraceMetadata(
84
+ schema_version="1.0",
85
+ algorithm=self.algo_name,
86
+ topology=grafo_name,
87
+ tag=self.trace_tag,
88
+ execution_date=timestamp,
89
+ parameters={
90
+ "max_time": self.engine.maxtime,
91
+ "total_nodes": total_nodes,
92
+ "total_edges": total_edges
93
+ },
94
+ metrics=self.execution_metrics
95
+ )
96
+
97
+ if self.collector:
98
+ self.collector.dump(trace_path, metadata)
99
+
100
+ def run(self) -> None:
101
+ """Punto de entrada principal para ejecutar la simulación y guardar resultados."""
102
+ self._execute()
103
+ if self.trace:
104
+ self._save_trace()
risansym/simulator.py ADDED
@@ -0,0 +1,73 @@
1
+ import heapq
2
+ from typing import TYPE_CHECKING
3
+
4
+ from risansym.event import Event
5
+ from risansym.schemas import TransmitEvent, ReceiveEvent, AppLogEvent
6
+
7
+ if TYPE_CHECKING:
8
+ from risansym.trace import TraceCollector
9
+
10
+
11
+ class Simulator:
12
+ """Motor de simulación dirigido por eventos, coordinado por heap de mínimos."""
13
+
14
+ def __init__(self, maxtime: float, debug: bool = True, collector: 'TraceCollector | None' = None) -> None:
15
+ self.clock: float = 0.0
16
+ self.maxtime: float = maxtime
17
+ self._agenda: list[Event] = []
18
+ self.debug: bool = debug
19
+ self._collector = collector
20
+
21
+ def insert_event(self, event: Event) -> None:
22
+ """Inserta un evento en el heap si está dentro del horizonte temporal."""
23
+ if event.time <= self.maxtime:
24
+ heapq.heappush(self._agenda, event)
25
+ if self.debug:
26
+ print(f"[t={self.clock:.1f}] Nodo {event.source} TRANSMITE '{event.name}' -> Nodo {event.target} (llegará en t={event.time:.1f})")
27
+
28
+ if self._collector:
29
+ self._collector.record(TransmitEvent(
30
+ action="TRANSMIT",
31
+ clock=self.clock,
32
+ event_time=event.time,
33
+ source=event.source,
34
+ target=event.target,
35
+ name=event.name,
36
+ payload=event.payload
37
+ ))
38
+
39
+ def pop_event(self) -> Event:
40
+ """Extrae el evento más próximo y avanza el reloj global."""
41
+ event = heapq.heappop(self._agenda)
42
+ self.clock = event.time
43
+ if self.debug:
44
+ print(f"[t={self.clock:.1f}] Nodo {event.target} RECIBE '{event.name}' <- Nodo {event.source}")
45
+
46
+ if self._collector:
47
+ self._collector.record(ReceiveEvent(
48
+ action="RECEIVE",
49
+ clock=self.clock,
50
+ source=event.source,
51
+ target=event.target,
52
+ name=event.name,
53
+ payload=event.payload
54
+ ))
55
+ return event
56
+
57
+ def log_app_event(self, source: int, message: str) -> None:
58
+ """Registra un evento lógico de aplicación en la traza."""
59
+ if self.debug:
60
+ print(f"[t={self.clock:.1f}] APP Nodo {source}: {message}")
61
+
62
+ if self._collector:
63
+ self._collector.record(AppLogEvent(
64
+ action="APP_LOG",
65
+ clock=self.clock,
66
+ source=source,
67
+ message=message
68
+ ))
69
+
70
+ @property
71
+ def is_on(self) -> bool:
72
+ """True mientras existan eventos pendientes en la agenda."""
73
+ return bool(self._agenda)
risansym/trace.py ADDED
@@ -0,0 +1,28 @@
1
+ import json
2
+ from pathlib import Path
3
+ from risansym.schemas import TraceEvent, TraceMetadata, TraceOutput
4
+
5
+ class TraceCollector:
6
+ """Responsable de acumular y persistir la traza de eventos simulados usando Pydantic."""
7
+
8
+ def __init__(self) -> None:
9
+ self._trace: list[TraceEvent] = []
10
+
11
+ def record(self, entry: TraceEvent) -> None:
12
+ """Añade un evento estructurado a la traza en memoria."""
13
+ self._trace.append(entry)
14
+
15
+ def get_event_count(self) -> int:
16
+ """Retorna el número de eventos registrados."""
17
+ return len(self._trace)
18
+
19
+ def dump(self, filepath: Path, metadata: TraceMetadata) -> None:
20
+ """Persiste la traza validada matemáticamente en un archivo JSON."""
21
+ filepath.parent.mkdir(parents=True, exist_ok=True)
22
+
23
+ # Validar y armar el objeto final
24
+ output = TraceOutput(metadata=metadata, trace=self._trace)
25
+
26
+ # Guardar en disco
27
+ with filepath.open('w', encoding='utf-8') as f:
28
+ f.write(output.model_dump_json(indent=2))
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: risansym
3
+ Version: 0.1.0
4
+ Summary: A Discrete Event Simulator for Distributed Systems
5
+ Project-URL: Homepage, https://github.com/PeraltaHD4K/risansym
6
+ Project-URL: Repository, https://github.com/PeraltaHD4K/risansym.git
7
+ Author-email: Diego Peralta Huerta <PeraltaHD4K@users.noreply.github.com>
8
+ Requires-Python: >=3.10
9
+ Requires-Dist: pydantic>=2.0.0
10
+ Description-Content-Type: text/markdown
11
+
12
+ # Risansym
13
+
14
+ A discrete event simulator for distributed systems algorithms.
15
+
16
+ ## Instalación
17
+
18
+ ```bash
19
+ pip install risansym
20
+ ```
@@ -0,0 +1,11 @@
1
+ risansym/__init__.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
2
+ risansym/event.py,sha256=0ej1JgDxYlEjOqG49Hj-_Y1kAp-_TRMybDMhwxnfNUk,454
3
+ risansym/model.py,sha256=15JgcToLsyaJyMq2pSl9BY3kKK8u1n65pKTaXTt7L6c,1347
4
+ risansym/process.py,sha256=3hHn913RcMu3K2YdqsxZtom_7FFySvTA-Jd8QbhgE48,998
5
+ risansym/schemas.py,sha256=3QiMTPgSrDgpJN04VnwIS2n6rh0hhkvlxnJVZfh84V0,1164
6
+ risansym/simulation.py,sha256=XSAukcFil9cEEMJDSsiaKaq65-Q8EQ3nxopPVvm1w0E,4048
7
+ risansym/simulator.py,sha256=I3-0HQYgeaZa5xOjAi16KSMhSOkkGIW_lLJguM0JCRc,2668
8
+ risansym/trace.py,sha256=q_nmgvXG6bJJ1DPEHuYxYp0DffABU2hMofHjdO6TuKg,1059
9
+ risansym-0.1.0.dist-info/METADATA,sha256=ZqMyd6ivgYv_CNRzBdaveugtelX5Knc6oD27rEBlpQs,540
10
+ risansym-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
11
+ risansym-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any