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 +1 -0
- risansym/event.py +14 -0
- risansym/model.py +44 -0
- risansym/process.py +34 -0
- risansym/schemas.py +41 -0
- risansym/simulation.py +104 -0
- risansym/simulator.py +73 -0
- risansym/trace.py +28 -0
- risansym-0.1.0.dist-info/METADATA +20 -0
- risansym-0.1.0.dist-info/RECORD +11 -0
- risansym-0.1.0.dist-info/WHEEL +4 -0
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,,
|