cortexforge 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.
Files changed (46) hide show
  1. cortexforge/cli/__init__.py +0 -0
  2. cortexforge/cli/forge.py +58 -0
  3. cortexforge/cli/planner.py +45 -0
  4. cortexforge/datasets/__init__.py +17 -0
  5. cortexforge/datasets/api.py +88 -0
  6. cortexforge/datasets/hash.py +11 -0
  7. cortexforge/datasets/local.py +87 -0
  8. cortexforge/datasets/manifest.py +68 -0
  9. cortexforge/datasets/types.py +33 -0
  10. cortexforge/forge/__init__.py +0 -0
  11. cortexforge/forge/main.py +25 -0
  12. cortexforge/forge/radio/__init__.py +0 -0
  13. cortexforge/forge/radio/rx.py +100 -0
  14. cortexforge/forge/radio/rx_recorder.py +40 -0
  15. cortexforge/forge/radio/tx.py +77 -0
  16. cortexforge/forge/radio/tx_burst.py +107 -0
  17. cortexforge/forge/radio/waveforms.py +42 -0
  18. cortexforge/forge/radio/waveforms_analog.py +85 -0
  19. cortexforge/forge/radio/waveforms_numerique.py +247 -0
  20. cortexforge/forge/utils/__init__.py +0 -0
  21. cortexforge/forge/utils/compute_baseline.py +64 -0
  22. cortexforge/forge/utils/load_timeline.py +36 -0
  23. cortexforge/forge/utils/node_identity.py +15 -0
  24. cortexforge/forge/utils/node_layout.py +45 -0
  25. cortexforge/forge/utils/sigmf/hash.py +10 -0
  26. cortexforge/forge/utils/sigmf/sigmf_annotations.py +90 -0
  27. cortexforge/forge/utils/sigmf/sigmf_captures.py +21 -0
  28. cortexforge/forge/utils/sigmf/sigmf_global.py +13 -0
  29. cortexforge/forge/utils/sigmf_writer.py +58 -0
  30. cortexforge/forge/utils/sync_barrier/rx_barrier_server.py +46 -0
  31. cortexforge/forge/utils/sync_barrier/sync_config.py +8 -0
  32. cortexforge/forge/utils/sync_barrier/tx_barrier_client.py +38 -0
  33. cortexforge/forge/utils/uhd_time.py +15 -0
  34. cortexforge/planner/__init__.py +0 -0
  35. cortexforge/planner/generators/__init__.py +0 -0
  36. cortexforge/planner/generators/cortexlab_scenario.py +44 -0
  37. cortexforge/planner/generators/experiment_scenario.py +191 -0
  38. cortexforge/planner/main.py +57 -0
  39. cortexforge/utils/__init__.py +0 -0
  40. cortexforge/utils/loader.py +35 -0
  41. cortexforge/utils/logger.py +21 -0
  42. cortexforge-0.1.0.dist-info/METADATA +16 -0
  43. cortexforge-0.1.0.dist-info/RECORD +46 -0
  44. cortexforge-0.1.0.dist-info/WHEEL +5 -0
  45. cortexforge-0.1.0.dist-info/entry_points.txt +3 -0
  46. cortexforge-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,90 @@
1
+ import math
2
+
3
+ from cortexforge.forge.utils.node_layout import distance
4
+ from cortexforge.forge.utils.node_identity import get_node_name
5
+ from cortexforge.forge.utils.compute_baseline import MIN_LINEAR_POWER, measure_window_power
6
+
7
+
8
+ def make_sigmf_annotations(annotations):
9
+ return sorted(annotations, key=lambda a: a["core:sample_start"])
10
+
11
+
12
+ def theoretical_bandwidth_hz(ev):
13
+ modulation = ev["modulation"].upper()
14
+ symbol_rate = float(ev["symbol_rate"])
15
+ rolloff = float(ev["rolloff"])
16
+
17
+ # Default occupied RF bandwidth.
18
+ bw = (1.0 + rolloff) * symbol_rate
19
+
20
+ if modulation == "AM-SSB":
21
+ return 0.5 * bw
22
+ # Règle de Carson
23
+ if modulation == "FM":
24
+ sample_rate = float(ev["sample_rate_sps"])
25
+ freq_dev_hz = min(symbol_rate * 0.25, sample_rate * 0.20)
26
+ message_bw = 0.5 * bw
27
+ return 2.0 * (freq_dev_hz + message_bw)
28
+
29
+ return bw
30
+
31
+
32
+ def timeline_to_sigmf_annotations(
33
+ events, rx_sample_rate, rx_uhd_t0, tx_center_freq, tx_gain, rx_data_path=None, baseline_stat=None
34
+ ):
35
+ ann = []
36
+ baseline_mean_power = None
37
+ if baseline_stat is not None:
38
+ baseline_mean_power = float(baseline_stat.get("mean_power", 0.0))
39
+
40
+ for ev in events:
41
+ start = int((ev["t_start_s"] - rx_uhd_t0) * rx_sample_rate)
42
+ count = int(ev["duration_s"] * rx_sample_rate)
43
+
44
+ modulation = ev["modulation"].upper()
45
+ bw = theoretical_bandwidth_hz(ev)
46
+ f0 = tx_center_freq
47
+
48
+ if modulation == "AM-SSB":
49
+ f_low = f0
50
+ f_high = f0 + bw
51
+ else:
52
+ f_low = f0 - bw / 2.0
53
+ f_high = f0 + bw / 2.0
54
+
55
+ annotation = {
56
+ "core:sample_start": start,
57
+ "core:sample_count": count,
58
+ "core:freq_lower_edge": f_low,
59
+ "core:freq_upper_edge": f_high,
60
+ "core:label": ev["modulation"],
61
+ "cortexforge:transmitter": ev["radio"],
62
+ "cortexforge:distance_m": distance(ev["radio"], get_node_name()),
63
+ "cortexforge:tx_gain_db": tx_gain,
64
+ "cortexforge:amplitude": ev["amplitude"],
65
+ "cortexforge:symbol_rate": ev["symbol_rate"],
66
+ "cortexforge:rolloff": ev["rolloff"],
67
+ }
68
+
69
+ if rx_data_path is not None and baseline_mean_power is not None and start >= 0 and count > 0:
70
+ try:
71
+ burst_stats = measure_window_power(
72
+ rx_data_path,
73
+ sample_start=start,
74
+ sample_count=count,
75
+ )
76
+ total_mean_power = burst_stats["mean_power"]
77
+ signal_mean_power = max(total_mean_power - baseline_mean_power, MIN_LINEAR_POWER)
78
+ annotation["cortexforge:rx_total_power_dbfs"] = burst_stats["power_dbfs"]
79
+ annotation["cortexforge:rx_signal_power_dbfs"] = (
80
+ 10.0 * math.log10(signal_mean_power / 2.0)
81
+ )
82
+ annotation["cortexforge:snr_db"] = 10.0 * math.log10(
83
+ signal_mean_power / max(baseline_mean_power, MIN_LINEAR_POWER)
84
+ )
85
+ except ValueError:
86
+ pass
87
+
88
+ ann.append(annotation)
89
+
90
+ return make_sigmf_annotations(ann)
@@ -0,0 +1,21 @@
1
+ from datetime import datetime, timezone
2
+
3
+ from cortexforge.forge.utils.node_identity import get_node_name
4
+
5
+
6
+ def make_sigmf_captures(center_freq: float, gain: float, hardware: str, stat):
7
+ return [
8
+ {
9
+ "core:datetime": datetime.now(timezone.utc).isoformat().replace("+00:00", "Z"),
10
+ "core:frequency": float(center_freq),
11
+ "core:sample_start": 0,
12
+ "core:hw": hardware,
13
+ "cortexforge:node": get_node_name(),
14
+ "cortexforge:gain": gain,
15
+ "cortexforge:baseline": {
16
+ "sample_start": stat["skip_samples"],
17
+ "sample_count": stat["win_samples"],
18
+ "power_dbfs": stat["power_dbfs"],
19
+ },
20
+ }
21
+ ]
@@ -0,0 +1,13 @@
1
+ from cortexforge.forge.utils.sigmf.hash import _sha512_hex
2
+
3
+
4
+ def make_sigmf_global(author: str, sample_rate: float, data_path, description: str):
5
+ return {
6
+ "core:author": author,
7
+ "core:description": description,
8
+ "core:recorder": "CorteXForge",
9
+ "core:datatype": "cf32_le",
10
+ "core:sample_rate": float(sample_rate),
11
+ "core:data_file": data_path.name,
12
+ "core:sha512": _sha512_hex(data_path),
13
+ }
@@ -0,0 +1,58 @@
1
+ from json import dumps
2
+ from pathlib import Path
3
+
4
+ from cortexforge.forge.utils.sigmf.sigmf_annotations import make_sigmf_annotations
5
+ from cortexforge.forge.utils.sigmf.sigmf_captures import make_sigmf_captures
6
+ from cortexforge.forge.utils.sigmf.sigmf_global import make_sigmf_global
7
+
8
+
9
+ def write_sigmf(
10
+ base_path: str,
11
+ data_file: str,
12
+ sample_rate: float,
13
+ center_freq: float,
14
+ gain: float,
15
+ stat,
16
+ annotations,
17
+ hardware: str,
18
+ description: str = "CorteXForge capture",
19
+ author: str = "CorteXForge",
20
+ ):
21
+ """
22
+ base_path: path without extension
23
+ data_file: existing IQ file path (fc32 raw)
24
+ Creates:
25
+ - base_path.sigmf-data
26
+ - base_path.sigmf-meta
27
+ """
28
+ base = Path(base_path)
29
+ data_src = Path(data_file)
30
+
31
+ meta_path = base.with_suffix(".sigmf-meta")
32
+ data_path = base.with_suffix(".sigmf-data")
33
+
34
+ # Move the data to .sigmf-data if needed
35
+ if data_src.resolve() != data_path.resolve():
36
+ data_path.parent.mkdir(parents=True, exist_ok=True)
37
+ data_src.replace(data_path)
38
+
39
+
40
+ meta = {
41
+ "global": make_sigmf_global(
42
+ author=author,
43
+ sample_rate=sample_rate,
44
+ data_path=data_path,
45
+ description=description,
46
+ ),
47
+ "captures": make_sigmf_captures(
48
+ center_freq=center_freq,
49
+ gain=gain,
50
+ hardware=hardware,
51
+ stat=stat,
52
+ ),
53
+ "annotations": make_sigmf_annotations(annotations),
54
+ }
55
+
56
+ meta_path.parent.mkdir(parents=True, exist_ok=True)
57
+ meta_path.write_text(dumps(meta, indent=2))
58
+ return str(data_path), str(meta_path)
@@ -0,0 +1,46 @@
1
+ import json
2
+ import time
3
+ import zmq
4
+ from logging import getLogger
5
+ from cortexforge.forge.utils.sync_barrier.sync_config import SyncConfig
6
+
7
+ logger = getLogger(__name__)
8
+
9
+
10
+ class RxBarrierServer:
11
+ def __init__(self, cfg: SyncConfig):
12
+ self.cfg = cfg
13
+ self.ctx = zmq.Context.instance()
14
+
15
+ self.rep = self.ctx.socket(zmq.REP)
16
+ self.rep.bind(f"tcp://*:{cfg.port_reg}")
17
+ logger.info("[RX][REP] bind tcp://*:%s", cfg.port_reg)
18
+
19
+
20
+ self.pub = self.ctx.socket(zmq.PUB)
21
+ self.pub.bind(f"tcp://*:{cfg.port_pub}")
22
+ logger.info("[RX][PUB] bind tcp://*:%s", cfg.port_pub)
23
+
24
+ self.registered = set()
25
+
26
+
27
+ def wait_for_all(self):
28
+ logger.info("[RX] Waiting for %d TX registrations...", self.cfg.expected_tx)
29
+ while len(self.registered) < self.cfg.expected_tx:
30
+ raw = self.rep.recv()
31
+ now = time.time()
32
+ logger.info("[RX][REP][recv %.6f] %r", now, raw)
33
+
34
+ data = json.loads(raw.decode("utf-8"))
35
+ node = data.get("node", "unknown")
36
+ typ = data.get("type")
37
+ logger.info("[RX] HELLO from node=%s type=%s", node, typ)
38
+
39
+ self.registered.add(node)
40
+ reply = {"ok": True, "registered": sorted(self.registered)}
41
+ self.rep.send_string(json.dumps(reply))
42
+ logger.info("[RX][REP][send %.6f] %s", time.time(), reply)
43
+
44
+ def broadcast(self, payload: dict) -> None:
45
+ logger.info("[RX][PUB] broadcast %s", payload)
46
+ self.pub.send_string(json.dumps(payload))
@@ -0,0 +1,8 @@
1
+ from dataclasses import dataclass
2
+
3
+ @dataclass
4
+ class SyncConfig:
5
+ server_host: str
6
+ port_reg: int = 5555
7
+ port_pub: int = 5556
8
+ expected_tx: int = 3
@@ -0,0 +1,38 @@
1
+ import json
2
+ import time
3
+ import zmq
4
+ from logging import getLogger
5
+ from cortexforge.forge.utils.sync_barrier.sync_config import SyncConfig
6
+
7
+ logger = getLogger(__name__)
8
+
9
+
10
+ class TxBarrierClient:
11
+ def __init__(self, cfg: SyncConfig, node_name: str):
12
+ self.cfg = cfg
13
+ self.node_name = node_name
14
+ self.ctx = zmq.Context.instance()
15
+
16
+ self.req = self.ctx.socket(zmq.REQ)
17
+ ep_req = f"tcp://{cfg.server_host}:{cfg.port_reg}"
18
+ self.req.connect(ep_req)
19
+ logger.info("[TX:%s][REQ] connect %s", node_name, ep_req)
20
+
21
+ self.sub = self.ctx.socket(zmq.SUB)
22
+ ep_sub = f"tcp://{cfg.server_host}:{cfg.port_pub}"
23
+ self.sub.connect(ep_sub)
24
+ self.sub.setsockopt_string(zmq.SUBSCRIBE, "")
25
+ logger.info("[TX:%s][SUB] connect %s + SUBSCRIBE ''", node_name, ep_sub)
26
+
27
+ def register(self):
28
+ msg = {"type": "HELLO", "node": self.node_name}
29
+ self.req.send_string(json.dumps(msg))
30
+ logger.info("[TX:%s][REQ][send %.6f] %s", self.node_name, time.time(), msg)
31
+
32
+ rep = self.req.recv_string()
33
+ logger.info("[TX:%s][REQ][recv %.6f] %s", self.node_name, time.time(), rep)
34
+
35
+ def wait_broadcast(self):
36
+ raw = self.sub.recv_string()
37
+ logger.info("[TX:%s][SUB][recv %.6f] %s", self.node_name, time.time(), raw)
38
+ return json.loads(raw)
@@ -0,0 +1,15 @@
1
+ import time
2
+ from gnuradio import uhd
3
+
4
+ def wait_for_pps_edge(usrp_block, poll_s=0.0001):
5
+ last = usrp_block.get_time_last_pps().get_real_secs()
6
+ while True:
7
+ time.sleep(poll_s)
8
+ cur = usrp_block.get_time_last_pps().get_real_secs()
9
+ if cur != last:
10
+ return cur
11
+
12
+ def arm_time_reset_next_pps(usrp_block):
13
+ wait_for_pps_edge(usrp_block)
14
+ usrp_block.set_time_next_pps(uhd.time_spec(0.0))
15
+ wait_for_pps_edge(usrp_block)
File without changes
File without changes
@@ -0,0 +1,44 @@
1
+ import logging
2
+ from pathlib import Path
3
+ from typing import List
4
+
5
+ from yaml import safe_dump
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def generate_cortexlab_scenario(
11
+ nodes: List[str],
12
+ duration: int,
13
+ image: str,
14
+ rx_command: str,
15
+ tx_command: str,
16
+ description: str = "Dataset Generator",
17
+ output_path: str = "scenario.yaml",
18
+ ):
19
+ data = {"description": description, "duration": duration, "nodes": {}}
20
+ rx_node = nodes[0]
21
+ data["nodes"][rx_node.replace("mnode", "node")] = {
22
+ "container": [
23
+ {
24
+ "image": image,
25
+ "command": rx_command,
26
+ }
27
+ ]
28
+ }
29
+ for node in nodes[1:]:
30
+ data["nodes"][node.replace("mnode", "node")] = {
31
+ "container": [
32
+ {
33
+ "image": image,
34
+ "command": tx_command,
35
+ }
36
+ ]
37
+ }
38
+
39
+ out_path = Path(output_path)
40
+ out_path.parent.mkdir(parents=True, exist_ok=True)
41
+ with out_path.open("w") as f:
42
+ safe_dump(data, f, sort_keys=False)
43
+
44
+ logger.info(f"Cortexlab scenario written to {out_path}")
@@ -0,0 +1,191 @@
1
+ import random
2
+ from typing import List
3
+
4
+ import pandas as pd
5
+
6
+
7
+ class ExperimentScenario:
8
+ """
9
+ Generate a pseudo-random experiment schedule as a CSV.
10
+
11
+ Constraints:
12
+ - Signals start no earlier than warmup_time.
13
+ - Signals must end before or at the total experiment duration.
14
+ - The receiver node and its params are fixed for the whole experiment.
15
+ """
16
+
17
+ def __init__(
18
+ self,
19
+ nodes: List[str],
20
+ duration: float,
21
+ rx_sample_rate: int,
22
+ warmup_time: float = 2.0,
23
+ ):
24
+ if len(nodes) < 2:
25
+ raise ValueError("You must provide at least one RX node and one TX node.")
26
+
27
+ if warmup_time >= duration:
28
+ raise ValueError("warmup_time must be strictly less than total duration.")
29
+
30
+ self.nodes = nodes
31
+ self.duration = duration
32
+ self.rx_sample_rate = rx_sample_rate
33
+ self.warmup_time = warmup_time
34
+ self.rx_node = nodes[0]
35
+ self.tx_nodes = nodes[1:]
36
+
37
+ self.modulations = [
38
+ "AM-DSB",
39
+ "AM-SSB",
40
+ "FM",
41
+ "OOK",
42
+ "PAM4",
43
+ "4ASK",
44
+ "8ASK",
45
+ "BPSK",
46
+ "QPSK",
47
+ "8PSK",
48
+ "16PSK",
49
+ "32PSK",
50
+ "CPFSK",
51
+ "GFSK",
52
+ "16QAM",
53
+ "32QAM_RECT",
54
+ "32QAM_CROSS",
55
+ "64QAM",
56
+ "128QAM_RECT",
57
+ "128QAM_CROSS",
58
+ "256QAM",
59
+ ]
60
+
61
+ self.duration_range_s = (0.002, 0.020)
62
+
63
+ @staticmethod
64
+ def _intervals_overlap(
65
+ start_a: float,
66
+ duration_a: float,
67
+ start_b: float,
68
+ duration_b: float,
69
+ ) -> bool:
70
+ end_a = start_a + duration_a
71
+ end_b = start_b + duration_b
72
+ return start_a < end_b and start_b < end_a
73
+
74
+ def _find_non_overlapping_start(
75
+ self,
76
+ signal_duration: float,
77
+ scheduled_intervals: List[tuple],
78
+ rng: random.Random,
79
+ max_attempts: int = 1000,
80
+ ) -> float:
81
+ latest_start = self.duration - signal_duration
82
+ if latest_start < self.warmup_time:
83
+ raise ValueError(
84
+ "Signal duration is too large for the available experiment window."
85
+ )
86
+
87
+ for _ in range(max_attempts):
88
+ candidate_start = round(rng.uniform(self.warmup_time, latest_start), 6)
89
+
90
+ has_overlap = any(
91
+ self._intervals_overlap(
92
+ candidate_start,
93
+ signal_duration,
94
+ existing_start,
95
+ existing_duration,
96
+ )
97
+ for existing_start, existing_duration in scheduled_intervals
98
+ )
99
+
100
+ if not has_overlap:
101
+ return candidate_start
102
+
103
+ raise RuntimeError(
104
+ "Unable to place a non-overlapping signal. "
105
+ "Try reducing n_signals, reducing signal durations, "
106
+ "or increasing the experiment duration."
107
+ )
108
+
109
+ def generate_table(
110
+ self,
111
+ n_signals: int = 100,
112
+ allow_overlap: bool = True,
113
+ seed: int | None = None,
114
+ ) -> pd.DataFrame:
115
+ """
116
+ Generate a pandas DataFrame with the experiment timeline.
117
+
118
+ Args:
119
+ n_signals: Number of signals to generate.
120
+ allow_overlap: If False, generated signals will not overlap in time.
121
+ seed: Optional seed for reproducibility.
122
+ """
123
+ rng = random.Random(seed)
124
+
125
+ rows = []
126
+ scheduled_intervals = []
127
+
128
+ for _ in range(n_signals):
129
+ signal_node = rng.choice(self.tx_nodes)
130
+ signal_duration = round(rng.uniform(*self.duration_range_s), 6)
131
+ signal_modulation = rng.choice(self.modulations)
132
+
133
+ if allow_overlap:
134
+ signal_start_time = round(
135
+ rng.uniform(self.warmup_time, self.duration - signal_duration), 6
136
+ )
137
+ else:
138
+ signal_start_time = self._find_non_overlapping_start(
139
+ signal_duration=signal_duration,
140
+ scheduled_intervals=scheduled_intervals,
141
+ rng=rng,
142
+ )
143
+ scheduled_intervals.append((signal_start_time, signal_duration))
144
+
145
+ rows.append(
146
+ {
147
+ "radio": signal_node,
148
+ "start_time": signal_start_time,
149
+ "duration_s": signal_duration,
150
+ "modulation": signal_modulation,
151
+ "amplitude": 0.5,
152
+ "roll_off": 0.35,
153
+ "symbol_rate": 3_125_000,
154
+ "sample_rate_sps": 25_000_000,
155
+ }
156
+ )
157
+
158
+ df = pd.DataFrame(
159
+ rows,
160
+ columns=[
161
+ "radio",
162
+ "start_time",
163
+ "duration_s",
164
+ "modulation",
165
+ "amplitude",
166
+ "roll_off",
167
+ "symbol_rate",
168
+ "sample_rate_sps",
169
+ ],
170
+ )
171
+
172
+ df = df.sort_values("start_time").reset_index(drop=True)
173
+ return df
174
+
175
+ def to_csv(
176
+ self,
177
+ output_path: str,
178
+ n_signals: int = 100,
179
+ allow_overlap: bool = True,
180
+ seed: int | None = None,
181
+ ):
182
+ """
183
+ Generate the table and write it directly to a CSV file.
184
+ """
185
+ df = self.generate_table(
186
+ n_signals=n_signals,
187
+ allow_overlap=allow_overlap,
188
+ seed=seed,
189
+ )
190
+ df.index.name = "id"
191
+ df.to_csv(output_path)
@@ -0,0 +1,57 @@
1
+ from cortexforge.cli.planner import parse_args
2
+ from cortexforge.planner.generators.cortexlab_scenario import (
3
+ generate_cortexlab_scenario,
4
+ )
5
+ from cortexforge.planner.generators.experiment_scenario import ExperimentScenario
6
+ from cortexforge.utils.loader import load_nodes
7
+ from cortexforge.utils.logger import setup_logger
8
+
9
+ logger = setup_logger()
10
+
11
+
12
+ def main() -> None:
13
+ logger.info("Starting scenario generation...")
14
+ args = parse_args()
15
+ nodes = load_nodes(args.nodes_path)
16
+
17
+ scenario = ExperimentScenario(
18
+ nodes=nodes,
19
+ duration=args.duration,
20
+ rx_sample_rate=args.rx_sample_rate,
21
+ )
22
+
23
+ df = scenario.generate_table(n_signals=100, allow_overlap=args.overlapping, seed=42)
24
+ print(df.head())
25
+
26
+ scenario.to_csv(
27
+ "configs/timeline.csv", n_signals=100, allow_overlap=args.overlapping, seed=42
28
+ )
29
+
30
+ generate_cortexlab_scenario(
31
+ nodes=nodes,
32
+ duration=args.duration + 30,
33
+ image="ghcr.io/andreaj42/cortexforge:latest",
34
+ rx_command=(
35
+ f'bash -lc "cortexforge-forge rx '
36
+ f"--duration {args.duration} "
37
+ f"--frequency {args.rx_frequency} "
38
+ f"--gain {args.rx_gain} "
39
+ f"--sample-rate {args.rx_sample_rate} "
40
+ f"--output-path /cortexlab/homes/{args.username}/out/ "
41
+ f'--timeline /cortexlab/homes/{args.username}/timeline.csv"'
42
+ ),
43
+ tx_command=(
44
+ f'bash -lc "cortexforge-forge tx '
45
+ f"--timeline /cortexlab/homes/{args.username}/timeline.csv "
46
+ f"--record-node {nodes[0]} "
47
+ f"--frequency {args.rx_frequency} "
48
+ f'--gain {args.rx_gain}"'
49
+ ),
50
+ description="Dataset Generator",
51
+ output_path="configs/scenario.yaml",
52
+ )
53
+ logger.info("Scenario generation completed.")
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
File without changes
@@ -0,0 +1,35 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ import yaml
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def load_yaml(path: Path) -> dict:
10
+ """Load a YAML file and return its contents as a dictionary."""
11
+ if not path.exists():
12
+ raise FileNotFoundError(f"File not found: {path}")
13
+ with path.open("r") as f:
14
+ return yaml.safe_load(f)
15
+
16
+
17
+ def load_nodes(path: Path = "configs/nodes.yaml") -> list[str]:
18
+ """
19
+ Load node IDs from a YAML configuration configs/nodes.yaml file.
20
+
21
+ Expected YAML format:
22
+ nodes:
23
+ - id: mnode1
24
+ - id: mnode2
25
+ ...
26
+ """
27
+ data = load_yaml(path)
28
+
29
+ nodes = [node["id"] for node in data["nodes"]]
30
+
31
+ if len(nodes) < 2:
32
+ raise ValueError("At least two nodes are required")
33
+
34
+ logger.info("Loaded %d nodes: %s", len(nodes), nodes)
35
+ return nodes
@@ -0,0 +1,21 @@
1
+ import logging
2
+
3
+
4
+ def setup_logger(level: int = logging.INFO) -> logging.Logger:
5
+ """
6
+ Configure logging for CorteXforge.
7
+ """
8
+ logger = logging.getLogger()
9
+ logger.setLevel(level)
10
+
11
+ if not logger.handlers:
12
+ console_handler = logging.StreamHandler()
13
+ console_handler.setLevel(level)
14
+ formatter = logging.Formatter(
15
+ "[%(asctime)s] [%(levelname)s][%(filename)s]: %(message)s",
16
+ datefmt="%Y-%m-%d %H:%M:%S",
17
+ )
18
+ console_handler.setFormatter(formatter)
19
+ logger.addHandler(console_handler)
20
+
21
+ return logger
@@ -0,0 +1,16 @@
1
+ Metadata-Version: 2.4
2
+ Name: cortexforge
3
+ Version: 0.1.0
4
+ Summary: RF generator dataset based on SLICES/CorteXlab
5
+ Author-email: Andrea Joly <andrea.joly@inria.fr>
6
+ Requires-Python: >=3.10
7
+ Provides-Extra: planner
8
+ Requires-Dist: numpy==2.3.4; extra == "planner"
9
+ Requires-Dist: pandas==2.3.3; extra == "planner"
10
+ Requires-Dist: python-dateutil==2.9.0.post0; extra == "planner"
11
+ Requires-Dist: pytz==2025.2; extra == "planner"
12
+ Requires-Dist: PyYAML==6.0.3; extra == "planner"
13
+ Requires-Dist: six==1.17.0; extra == "planner"
14
+ Requires-Dist: tzdata==2025.2; extra == "planner"
15
+ Provides-Extra: forge
16
+ Requires-Dist: sigmf==1.2.12; extra == "forge"