Skip to content

Commit 9cd4dd7

Browse files
feat: initial CORNET framework implementation
0 parents  commit 9cd4dd7

62 files changed

Lines changed: 2995 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/test.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- name: Set up Python
16+
uses: actions/setup-python@v5
17+
with:
18+
python-version: "3.10"
19+
20+
- name: Install package
21+
run: pip install -e .[dev]
22+
23+
- name: Run tests
24+
run: pytest tests/ -v --tb=short

.gitignore

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
__pycache__/
2+
*.py[cod]
3+
*.egg-info/
4+
dist/
5+
build/
6+
.venv/
7+
venv/
8+
*.egg
9+
.pytest_cache/
10+
.coverage
11+
htmlcov/
12+
tasks/*/generated_launch_*.py
13+
tasks/*/leaderboard.json.bak.*
14+
tasks/*/results/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 RBCCPS, Indian Institute of Science
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# CORNET — Generic Co-Simulation Framework for Networked Robots
2+
3+
[![CI](https://github.com/rbccps-iisc/CORNET/actions/workflows/test.yml/badge.svg)](https://github.com/rbccps-iisc/CORNET/actions)
4+
5+
**CORNET** is a task-driven, plugin-based co-simulation framework that lets you run network-robotics experiments with a single config file and one command:
6+
7+
```bash
8+
python -m cornet tasks/pendulum_nr_control
9+
```
10+
11+
## Features
12+
13+
- **Unified config schema** — one YAML covers network (NS-3 5G NR or Mininet-WiFi) + robot (Gazebo + ROS 2)
14+
- **Plugin architecture** — swap network or robot backends without touching orchestration code
15+
- **Task-folder convention** — each experiment is fully self-contained under `tasks/<name>/`
16+
- **Parameter sweep** — declare axes in config; framework runs all combinations automatically
17+
- **EvalTool interface** — standardized metric extraction per task
18+
- **Experiment leaderboard** — append-only JSON + `rich` terminal viewer across runs
19+
20+
## Quickstart
21+
22+
```bash
23+
pip install cornet-framework
24+
# System prerequisites: NS-3 (with 5G NR), Mininet-WiFi, Gazebo Classic 11, ROS 2 Humble
25+
python -m cornet tasks/pendulum_nr_control
26+
python -m cornet view tasks/pendulum_nr_control
27+
```
28+
29+
See [docs/INSTALL.md](docs/INSTALL.md) for system prerequisites and [docs/GETTING_STARTED.md](docs/GETTING_STARTED.md) for a walkthrough.
30+
31+
## CORNET Family
32+
33+
| Version | Repo | Publication |
34+
|---------|------|-------------|
35+
| 1.0 | [srikrishna3118/CORNET](https://github.com/srikrishna3118/CORNET) | COMSNETS 2020 |
36+
| 2.0 | [rbccps-iisc/CORNET2.0](https://github.com/rbccps-iisc/CORNET2.0) | arXiv:2109.06979, COMSNETS 2022 |
37+
| 3.0 | [rbccps-iisc/CORNET3.0](https://github.com/rbccps-iisc/CORNET3.0) | PhD thesis (IISc, 2025) |
38+
| Flagship | **this repo** ||
39+
40+
See [docs/LINEAGE.md](docs/LINEAGE.md) for the full family history and BibTeX citations.
41+
42+
## License
43+
44+
MIT — see [LICENSE](LICENSE).

cornet/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
"""CORNET: Generic Co-Simulation Framework for Networked Robots."""
2+
3+
__version__ = "0.1.0"

cornet/__main__.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Entry point: python -m cornet tasks/<name> or python -m cornet view tasks/<name>"""
2+
3+
import argparse
4+
import sys
5+
6+
7+
def main() -> None:
8+
parser = argparse.ArgumentParser(
9+
prog="cornet",
10+
description="CORNET co-simulation framework",
11+
)
12+
subparsers = parser.add_subparsers(dest="command")
13+
14+
# run subcommand (default when a path is given directly)
15+
run_parser = subparsers.add_parser("run", help="Run an experiment task")
16+
run_parser.add_argument("task", help="Path to task directory or config.yaml")
17+
18+
# view subcommand
19+
view_parser = subparsers.add_parser("view", help="View experiment leaderboard")
20+
view_parser.add_argument("task", help="Path to task directory")
21+
view_parser.add_argument(
22+
"--higher-is-better",
23+
action="store_true",
24+
default=False,
25+
help="Sort leaderboard descending (higher metric = better)",
26+
)
27+
28+
# Allow bare positional: `python -m cornet tasks/foo` treated as run
29+
parser.add_argument("_task_positional", nargs="?", help=argparse.SUPPRESS)
30+
31+
args = parser.parse_args()
32+
33+
if args.command == "view":
34+
from cornet.leaderboard.viewer import show
35+
show(args.task, higher_is_better=args.higher_is_better)
36+
37+
elif args.command == "run":
38+
from cornet.orchestrator import Orchestrator
39+
orch = Orchestrator()
40+
orch.run(task_dir=args.task)
41+
42+
elif args._task_positional:
43+
# bare positional: treat as run
44+
from cornet.orchestrator import Orchestrator
45+
orch = Orchestrator()
46+
orch.run(task_dir=args._task_positional)
47+
48+
else:
49+
parser.print_help()
50+
sys.exit(1)
51+
52+
53+
if __name__ == "__main__":
54+
main()

cornet/config/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Config sub-package

cornet/config/loader.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
"""Config loader for CORNET unified schema.
2+
3+
Usage:
4+
from cornet.config.loader import load_unified
5+
cfg = load_unified("tasks/my_task/config.yaml")
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from pathlib import Path
11+
12+
import yaml
13+
14+
from cornet.config.schema import ConfigValidationError, UnifiedConfig
15+
16+
17+
def load_unified(path: str | Path) -> UnifiedConfig:
18+
"""Load and validate a unified-v1 YAML config file.
19+
20+
Raises:
21+
ConfigValidationError: if the file is not unified-v1 or fails validation.
22+
FileNotFoundError: if *path* does not exist.
23+
"""
24+
p = Path(path)
25+
if not p.exists():
26+
raise FileNotFoundError(f"Config not found: {p}")
27+
28+
with p.open() as fh:
29+
raw = yaml.safe_load(fh)
30+
31+
if not isinstance(raw, dict):
32+
raise ConfigValidationError(f"Config must be a YAML mapping, got {type(raw).__name__}")
33+
34+
schema_tag = raw.get("_schema", "")
35+
if schema_tag != "unified-v1":
36+
raise ConfigValidationError(
37+
f"Expected '_schema: unified-v1', got '{schema_tag}'. "
38+
"This loader only handles unified configs."
39+
)
40+
41+
try:
42+
return UnifiedConfig.model_validate(raw)
43+
except Exception as exc:
44+
raise ConfigValidationError(str(exc)) from exc
45+
46+
47+
def is_unified(path: str | Path) -> bool:
48+
"""Return True if the YAML file at *path* declares ``_schema: unified-v1``."""
49+
p = Path(path)
50+
if not p.exists():
51+
return False
52+
try:
53+
with p.open() as fh:
54+
raw = yaml.safe_load(fh)
55+
return isinstance(raw, dict) and raw.get("_schema") == "unified-v1"
56+
except Exception:
57+
return False

cornet/config/schema.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
"""Pydantic v2 unified config schema for CORNET experiments."""
2+
3+
from __future__ import annotations
4+
5+
from pathlib import Path
6+
from typing import Any, Literal
7+
8+
from pydantic import BaseModel, ConfigDict, field_validator, model_validator
9+
10+
11+
class ConfigValidationError(ValueError):
12+
"""Raised when a unified config fails schema validation."""
13+
14+
15+
# ── Node models ──────────────────────────────────────────────────────────────
16+
17+
class ContainerConfig(BaseModel):
18+
image: str
19+
environment: dict[str, str] = {}
20+
volumes: list[str] = []
21+
22+
23+
class NodeConfig(BaseModel):
24+
name: str
25+
type: Literal["MOBILE", "STATIC", "UE", "GNB", "EPC"] = "MOBILE"
26+
container: ContainerConfig | None = None
27+
# Arbitrary extra keys (e.g. position, speed) are allowed
28+
model_config = ConfigDict(extra="allow")
29+
30+
31+
# ── Network section ───────────────────────────────────────────────────────────
32+
33+
class MininetConfig(BaseModel):
34+
wmediumd: bool = False
35+
ssid: str = "cornet-net"
36+
mode: str = "g"
37+
channel: int = 1
38+
39+
40+
class NetworkConfig(BaseModel):
41+
plugin: str
42+
type: Literal["ns3", "mininet", "ns3+mininet"]
43+
nodes: list[NodeConfig] = []
44+
mininet: MininetConfig | None = None
45+
# NS-3 specific keys forwarded as-is
46+
model_config = ConfigDict(extra="allow")
47+
48+
49+
# ── Robot section ─────────────────────────────────────────────────────────────
50+
51+
class ModelConfig(BaseModel):
52+
type: Literal["urdf", "sdf"]
53+
path: str # relative to task dir or absolute
54+
55+
56+
class PoseConfig(BaseModel):
57+
x: float = 0.0
58+
y: float = 0.0
59+
z: float = 0.0
60+
roll: float = 0.0
61+
pitch: float = 0.0
62+
yaw: float = 0.0
63+
64+
65+
class RobotEntry(BaseModel):
66+
name: str
67+
model: ModelConfig
68+
pose: PoseConfig = PoseConfig()
69+
ros_namespace: str | None = None
70+
71+
72+
class RobotConfig(BaseModel):
73+
plugin: str
74+
launch_file: str | None = None
75+
world: str | None = None
76+
robots: list[RobotEntry] = []
77+
78+
79+
# ── Experiment section ────────────────────────────────────────────────────────
80+
81+
class SweepConfig(BaseModel):
82+
axes: dict[str, list[Any]] # e.g. {"network.numerology": [2,4], "network.bandwidth": [20,40]}
83+
parallel: bool = False
84+
repeats: int = 1
85+
86+
@field_validator("repeats")
87+
@classmethod
88+
def _positive_repeats(cls, v: int) -> int:
89+
if v < 1:
90+
raise ConfigValidationError("sweep.repeats must be >= 1")
91+
return v
92+
93+
94+
class ExperimentConfig(BaseModel):
95+
name: str
96+
duration: float # seconds
97+
output_dir: str = "results"
98+
primary_metric: str | None = None
99+
higher_is_better: bool = False
100+
sweep: SweepConfig | None = None
101+
102+
103+
# ── Top-level ─────────────────────────────────────────────────────────────────
104+
105+
class UnifiedConfig(BaseModel):
106+
"""Root config model for CORNET unified schema (``_schema: unified-v1``)."""
107+
108+
_schema: str = "unified-v1" # literal marker; not validated by pydantic
109+
network: NetworkConfig
110+
robot: RobotConfig
111+
experiment: ExperimentConfig
112+
113+
@model_validator(mode="after")
114+
def _cross_validate(self) -> "UnifiedConfig":
115+
# Mininet wmediumd requires Docker support — warn but don't error
116+
return self

cornet/context.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"""ExperimentContext — shared state passed between plugins during a run."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass, field
6+
7+
8+
@dataclass
9+
class NetworkContext:
10+
"""State populated by the network plugin after start()."""
11+
12+
# Maps node/UE name → assigned IP address (set by network plugin after start)
13+
node_ips: dict[str, str] = field(default_factory=dict)
14+
15+
16+
@dataclass
17+
class RobotContext:
18+
"""State populated by the robot plugin after start()."""
19+
20+
# Maps robot name → spawned namespace (set by robot plugin after start)
21+
robot_namespaces: dict[str, str] = field(default_factory=dict)
22+
23+
24+
@dataclass
25+
class ExperimentContext:
26+
"""Top-level context object shared across all plugins in a single run."""
27+
28+
network: NetworkContext = field(default_factory=NetworkContext)
29+
robot: RobotContext = field(default_factory=RobotContext)
30+
31+
# Variant ID for sweep runs (e.g. "numerology=2_bandwidth=40"); "default" otherwise
32+
variant_id: str = "default"

0 commit comments

Comments
 (0)