adetuire1 commited on
Commit
fba140f
·
verified ·
1 Parent(s): 9ac3f64

Upload folder using huggingface_hub

Browse files
scripts/augment_ops_coverage.py ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ augment_ops_coverage.py
4
+ Create op-augmented plans from a cleaned AttackPlan JSONL.
5
+
6
+ Usage:
7
+ %run scripts/augment_ops_coverage.py --src scripts/train_attackplan.filtered.jsonl --sem kb/specs/point_semantics.json --out scripts/train_attackplan.aug.jsonl
8
+ """
9
+ from __future__ import annotations
10
+ import argparse, json, random
11
+ from pathlib import Path
12
+
13
+ def load_sem(p): return json.loads(Path(p).read_text(encoding="utf-8"))
14
+
15
+ def coerce_num(x):
16
+ if isinstance(x, (int, float)): return float(x)
17
+ try: return float(str(x))
18
+ except Exception: return None
19
+
20
+ def make_numeric_variants(it, spec, rng):
21
+ base = []
22
+ v = it.get("attack_value")
23
+ num = coerce_num(v)
24
+ if num is None: return base
25
+ ops = spec.get("ops", [])
26
+ inc = spec.get("numeric_increase_pct", 0.10)
27
+ dec = spec.get("numeric_decrease_pct", 0.10)
28
+ scales = spec.get("numeric_scale_factors", [0.5, 1.5])
29
+
30
+ if "increase" in ops:
31
+ j = dict(it); j["op"] = "increase"; j["attack_value"] = round(num * (1 + inc), 3); base.append(j)
32
+ if "decrease" in ops:
33
+ j = dict(it); j["op"] = "decrease"; j["attack_value"] = round(num * (1 - dec), 3); base.append(j)
34
+ if "scale" in ops:
35
+ for s in scales:
36
+ j = dict(it); j["op"] = "scale"; j["attack_value"] = round(num * s, 3); base.append(j)
37
+ return base
38
+
39
+ def make_enum_variants(it, spec):
40
+ base = []
41
+ vals = [str(v).upper() for v in spec.get("values", [])]
42
+ if not vals: return base
43
+ cur = str(it.get("attack_value", "")).upper()
44
+ if "open" in spec.get("ops", []):
45
+ j = dict(it); j["op"] = "open"; j["attack_value"] = "OPEN"; base.append(j)
46
+ if "close" in spec.get("ops", []):
47
+ j = dict(it); j["op"] = "close"; j["attack_value"] = "CLOSED"; base.append(j)
48
+ if "trip" in spec.get("ops", []) and "OPEN" in vals:
49
+ j = dict(it); j["op"] = "trip"; j["attack_value"] = "OPEN"; base.append(j)
50
+ return base
51
+
52
+ def main():
53
+ ap = argparse.ArgumentParser()
54
+ ap.add_argument("--src", required=True)
55
+ ap.add_argument("--sem", required=True)
56
+ ap.add_argument("--out", required=True)
57
+ ap.add_argument("--seed", type=int, default=7)
58
+ ap.add_argument("--max_aug_per_item", type=int, default=2, help="cap how many variants per original item")
59
+ args = ap.parse_args()
60
+
61
+ rng = random.Random(args.seed)
62
+ sem = load_sem(args.sem)
63
+ props = sem.get("properties", {})
64
+ defaults = sem.get("defaults", {})
65
+
66
+ src_lines = Path(args.src).read_text(encoding="utf-8-sig").splitlines()
67
+ out = []
68
+ n_src = 0; n_aug_plans = 0
69
+
70
+ for ln in src_lines:
71
+ if not ln.strip(): continue
72
+ plan = json.loads(ln)
73
+ n_src += 1
74
+ out.append(ln) # keep original
75
+
76
+ # Make a simple one-item plan per augmented variant for clarity
77
+ for it in plan.get("plan", []):
78
+ point = it.get("point","")
79
+ spec = dict(defaults); spec.update(props.get(point, {}))
80
+ t = spec.get("type")
81
+ variants = []
82
+ if t in {"number","number_or_complex"}:
83
+ variants = make_numeric_variants(it, spec, rng)
84
+ elif t == "enum":
85
+ variants = make_enum_variants(it, spec)
86
+
87
+ rng.shuffle(variants)
88
+ for v in variants[:args.max_aug_per_item]:
89
+ out.append(json.dumps({
90
+ "version": plan.get("version","1.1"),
91
+ "time": plan.get("time", {"start_s":0,"end_s":60}),
92
+ "mim": plan.get("mim", {"active":True,"selected":["MIM1","MIM2","MIM3","MIM4"]}),
93
+ "plan": [v],
94
+ "compile_hints": plan.get("compile_hints", {"scenario_id":"a"})
95
+ }, ensure_ascii=False))
96
+ n_aug_plans += 1
97
+
98
+ Path(args.out).write_text("\n".join(out) + "\n", encoding="utf-8")
99
+ print(f"[done] plans_in={n_src}, aug_plans_added={n_aug_plans}, total_out={len(out)}")
100
+ print(f"[wrote] {Path(args.out).resolve()}")
101
+
102
+ if __name__ == "__main__":
103
+ main()
scripts/build_rag_index.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import argparse, json, os, glob
3
+ from pathlib import Path
4
+ import faiss
5
+ from sentence_transformers import SentenceTransformer
6
+ import numpy as np
7
+
8
+ def gather_docs():
9
+ items=[]
10
+ # JSON examples/snippets
11
+ for fp in glob.glob("kb/examples/*.json")+glob.glob("kb/snippets/json/*.json"):
12
+ try:
13
+ txt = Path(fp).read_text(encoding="utf-8")
14
+ items.append(("json", fp, txt))
15
+ except: pass
16
+ # GLM snippets as text
17
+ for fp in glob.glob("kb/snippets/glm/*"):
18
+ try:
19
+ if Path(fp).suffix.lower() in {".glm",".md",""}:
20
+ txt = Path(fp).read_text(encoding="utf-8")
21
+ items.append(("glm", fp, txt))
22
+ except: pass
23
+ # cheatsheet + semantics
24
+ for fp in ["kb/cheatsheets/property_glossary.md","kb/specs/point_semantics.json"]:
25
+ if Path(fp).exists():
26
+ items.append(("text", fp, Path(fp).read_text(encoding="utf-8")))
27
+ return items
28
+
29
+ def main():
30
+ ap = argparse.ArgumentParser()
31
+ ap.add_argument("--model", default="sentence-transformers/all-MiniLM-L6-v2") # small + fast
32
+ ap.add_argument("--outdir", default="rag_index")
33
+ args = ap.parse_args()
34
+
35
+ os.makedirs(args.outdir, exist_ok=True)
36
+ docs = gather_docs()
37
+ if not docs: raise SystemExit("No docs to index.")
38
+ model = SentenceTransformer(args.model)
39
+ corpus = [f"[{k}] {p}\n{t}" for (k,p,t) in docs]
40
+ emb = model.encode(corpus, normalize_embeddings=True, convert_to_numpy=True, show_progress_bar=True)
41
+ dim = emb.shape[1]
42
+ index = faiss.IndexFlatIP(dim)
43
+ index.add(emb)
44
+ faiss.write_index(index, str(Path(args.outdir, "kb.faiss")))
45
+ Path(args.outdir, "kb_meta.json").write_text(json.dumps(
46
+ {"paths":[p for (_,p,_) in docs], "model": args.model}, indent=2), encoding="utf-8")
47
+ print("[ok] indexed", len(docs), "docs to", Path(args.outdir,"kb.faiss").resolve())
48
+
49
+ if __name__ == "__main__":
50
+ main()
scripts/filter_attackplan_jsonl.py ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ filter_attackplan_jsonl.py
4
+ Align AttackPlan JSONL to kb/specs/point_semantics.json and GLM-backed encodings.
5
+
6
+ - Keeps capacitor phase switches (switchA/B/C) as enums.
7
+ - Normalizes real switch objects: switch.* {switchA|switchB|switchC} -> status (only for switches).
8
+ - Drops unknown/low-confidence points per semantics.
9
+ - Coerces numeric/complex vs enum values.
10
+ - Optionally rewrites regulator_X.tap_pos_* -> regulator_configuration_X.tap_pos_*
11
+
12
+ Usage:
13
+ python3 scripts/filter_attackplan_jsonl.py \
14
+ --src "C:/.../train_attackplan.jsonl" \
15
+ --sem "kb/specs/point_semantics.json" \
16
+ --out "scripts/train_attackplan.filtered.jsonl" \
17
+ --fix-switch --map-regcfg --debug
18
+ """
19
+ from __future__ import annotations
20
+ import argparse, json, re
21
+ from pathlib import Path
22
+
23
+ CAP_REGEX = re.compile(r'(^|[_.])cap_', re.I)
24
+ SW_REGEX = re.compile(r'(switch|microgrid_switch)', re.I)
25
+ REG_REGEX = re.compile(r'(^|[_.])regulator_(\d+)$', re.I)
26
+ RCF_REGEX = re.compile(r'(^|[_.])regulator_configuration_(\d+)$', re.I)
27
+
28
+ def load_semantics(p: Path) -> dict:
29
+ j = json.loads(p.read_text(encoding="utf-8"))
30
+ props = j.get("properties", {})
31
+ # resolve "like" references
32
+ for k, v in list(props.items()):
33
+ if "like" in v:
34
+ ref = props.get(v["like"], {})
35
+ merged = dict(ref); merged.update({kk: vv for kk, vv in v.items() if kk != "like"})
36
+ props[k] = merged
37
+ j["properties"] = props
38
+ return j
39
+
40
+ def is_complex_str(s: str) -> bool:
41
+ return bool(re.fullmatch(r"\s*-?\d+(\.\d+)?\s*\+\s*-?\d+(\.\d+)?j\s*", s))
42
+
43
+ def coerce_numeric(x):
44
+ if isinstance(x, (int, float)): return float(x)
45
+ try: return float(str(x))
46
+ except Exception: return None
47
+
48
+ def parse_name(item_name: str) -> tuple[str,str]:
49
+ """Return (device, point) from 'MIM2.device.point' or 'device.point'."""
50
+ parts = item_name.split(".")
51
+ if len(parts) >= 3 and parts[0].upper().startswith("MIM"):
52
+ return parts[1], parts[-1]
53
+ if len(parts) >= 2:
54
+ return parts[0], parts[-1]
55
+ return item_name, ""
56
+
57
+ def maybe_fix_switch_point(device: str, point: str) -> str:
58
+ """
59
+ - For capacitor objects (cap_*), KEEP switchA/B/C.
60
+ - For real switch objects, normalize switchA/B/C -> status.
61
+ """
62
+ if CAP_REGEX.search(device):
63
+ return point
64
+ if SW_REGEX.search(device) and point.lower() in {"switcha","switchb","switchc"}:
65
+ return "status"
66
+ return point
67
+
68
+ def maybe_map_reg_config(item_name: str) -> str:
69
+ """regulator_n.* -> regulator_configuration_n.* if not already mapped."""
70
+ if RCF_REGEX.search(item_name):
71
+ return item_name
72
+ m = REG_REGEX.search(item_name)
73
+ if not m:
74
+ return item_name
75
+ idx = m.group(2)
76
+ return REG_REGEX.sub(lambda _: f"regulator_configuration_{idx}", item_name)
77
+
78
+ def main():
79
+ ap = argparse.ArgumentParser()
80
+ ap.add_argument("--src", required=True)
81
+ ap.add_argument("--sem", required=True)
82
+ ap.add_argument("--out", required=True)
83
+ ap.add_argument("--allow-low", nargs="*", default=[], help="Points to allow even if confidence=low.")
84
+ ap.add_argument("--fix-switch", dest="fix_switch", action="store_true",
85
+ help="Normalize switch.* switchA/B/C -> status for real switches")
86
+ ap.add_argument("--map-regcfg", dest="map_regcfg", action="store_true",
87
+ help="Route regulator_X.* -> regulator_configuration_X.*")
88
+ ap.add_argument("--debug", action="store_true")
89
+ args = ap.parse_args()
90
+
91
+ src_path = Path(args.src)
92
+ sem_path = Path(args.sem)
93
+ if args.debug:
94
+ print("[debug] src:", src_path.resolve(), "exists:", src_path.exists())
95
+ print("[debug] sem:", sem_path.resolve(), "exists:", sem_path.exists())
96
+ print("[debug] out:", Path(args.out).resolve())
97
+
98
+ if not src_path.exists():
99
+ raise SystemExit(f"[error] src not found: {src_path}")
100
+ if not sem_path.exists():
101
+ raise SystemExit(f"[error] sem not found: {sem_path}")
102
+
103
+ sem = load_semantics(sem_path)
104
+ props = sem["properties"]
105
+ default_drop_low = sem.get("exclusions", {}).get("default_drop_if_confidence_low", True)
106
+ allow_low = set(sem.get("exclusions", {}).get("allow_low_confidence", []) + args.allow_low)
107
+
108
+ src_lines = src_path.read_text(encoding="utf-8-sig").splitlines()
109
+ out_lines = []
110
+ n_dropped_items = n_sw_fixed = n_reg_map = 0
111
+
112
+ for ln in src_lines:
113
+ if not ln.strip(): continue
114
+ try:
115
+ plan = json.loads(ln)
116
+ except Exception:
117
+ continue
118
+
119
+ new_items = []
120
+ for it in plan.get("plan", []):
121
+ name = it.get("name", "")
122
+ device, _ = parse_name(name)
123
+
124
+ # switch normalization
125
+ if args.fix_switch:
126
+ new_point = maybe_fix_switch_point(device, it.get("point",""))
127
+ if new_point != it.get("point"):
128
+ it["point"] = new_point
129
+ if "." in name:
130
+ parts = name.split(".")
131
+ parts[-1] = new_point
132
+ it["name"] = ".".join(parts)
133
+ n_sw_fixed += 1
134
+
135
+ # regulator mapping
136
+ if args.map_regcfg and it.get("point","").startswith("tap_pos_") and REG_REGEX.search(device):
137
+ old = it["name"]
138
+ it["name"] = maybe_map_reg_config(it["name"])
139
+ if it["name"] != old:
140
+ n_reg_map += 1
141
+
142
+ spec = props.get(it.get("point",""), {})
143
+ # policy: drop if unknown or low-confidence (unless allowed)
144
+ if not spec or spec.get("drop"):
145
+ n_dropped_items += 1; continue
146
+ if default_drop_low and spec.get("confidence") == "low" and it.get("point") not in allow_low:
147
+ n_dropped_items += 1; continue
148
+
149
+ typ = spec.get("type")
150
+ val = it.get("attack_value")
151
+
152
+ if typ == "enum":
153
+ sval = str(val).upper() if val is not None else ""
154
+ vals = [str(v).upper() for v in spec.get("values",[])]
155
+ if sval not in vals:
156
+ if sval in {"1","TRUE"} and "CLOSED" in vals:
157
+ it["attack_value"] = "CLOSED"
158
+ elif sval in {"0","FALSE"} and "OPEN" in vals:
159
+ it["attack_value"] = "OPEN"
160
+ else:
161
+ n_dropped_items += 1; continue
162
+
163
+ elif typ in {"number","integer"}:
164
+ num = coerce_numeric(val)
165
+ if num is None:
166
+ n_dropped_items += 1; continue
167
+ if typ == "integer":
168
+ num = int(round(num))
169
+ it["attack_value"] = num
170
+
171
+ elif typ == "number_or_complex":
172
+ if isinstance(val, str) and is_complex_str(val):
173
+ pass
174
+ else:
175
+ num = coerce_numeric(val)
176
+ if num is None:
177
+ n_dropped_items += 1; continue
178
+ it["attack_value"] = num
179
+
180
+ new_items.append(it)
181
+
182
+ if new_items:
183
+ plan["plan"] = new_items
184
+ out_lines.append(json.dumps(plan, ensure_ascii=False))
185
+
186
+ Path(args.out).write_text("\n".join(out_lines) + "\n", encoding="utf-8")
187
+ print(f"[done] plans_in={len(src_lines)}, plans_out={len(out_lines)}, "
188
+ f"items_dropped={n_dropped_items}, switches_normalized={n_sw_fixed}, regulators_mapped={n_reg_map}")
189
+ print(f"[wrote] {Path(args.out).resolve()}")
190
+
191
+ if __name__ == "__main__":
192
+ main()
scripts/guardrails.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # scripts/guardrails.py
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ import re
6
+ from pathlib import Path
7
+ from typing import Any, Dict, List, Optional, Tuple, Set
8
+
9
+
10
+ def _read_json(path: str) -> Dict[str, Any]:
11
+ return json.loads(Path(path).read_text(encoding="utf-8"))
12
+
13
+
14
+ class TargetRegistry:
15
+ def __init__(self, data: Dict[str, Any]):
16
+ self.data = data or {}
17
+ self.canonical = self.data.get("canonical", {})
18
+ self.points = self.data.get("points", {})
19
+ self.synonyms = self.data.get("synonyms", {})
20
+ self.mim_default_by_mg = self.data.get("mim_default_by_mg", {})
21
+
22
+ # Build quick lookup of allowed devices per mg (union of all groups)
23
+ self.allowed_by_mg: Dict[str, Set[str]] = {}
24
+ for group, mg_map in self.canonical.items():
25
+ for mg, devices in mg_map.items():
26
+ self.allowed_by_mg.setdefault(mg, set()).update(devices)
27
+
28
+ # Flatten device synonyms
29
+ self.dev_syn = self.synonyms.get("device", {})
30
+ self.point_syn = self.synonyms.get("point", {})
31
+ self.mim_syn = self.synonyms.get("mim", {})
32
+
33
+ @classmethod
34
+ def from_json(cls, path: str) -> "TargetRegistry":
35
+ return cls(_read_json(path))
36
+
37
+ def allowed_devices(self, mg: Optional[str]) -> Set[str]:
38
+ if not mg:
39
+ return set()
40
+ return self.allowed_by_mg.get(mg, set())
41
+
42
+ def canonicalize_mim(self, mim: Optional[str]) -> Optional[str]:
43
+ if mim is None:
44
+ return None
45
+ m = self.mim_syn.get(mim, mim)
46
+ m = m.upper()
47
+ if re.fullmatch(r"MIM[1-4]", m):
48
+ return m
49
+ return None # unknown MIM label
50
+
51
+ def canonicalize_point(self, point: str) -> str:
52
+ return self.point_syn.get(point, point)
53
+
54
+ def canonicalize_device(self, dev: str, mg: Optional[str]) -> str:
55
+ # direct synonym
56
+ dev2 = self.dev_syn.get(dev, dev)
57
+
58
+ # If device has a dot (like mg1.cap_01), squash using mg+core
59
+ if "." in dev2 and mg:
60
+ core = dev2.split(".")[-1]
61
+ candidate = f"{mg}{core}"
62
+ # Try mg-prefixed candidate first
63
+ if candidate in self.allowed_devices(mg):
64
+ return candidate
65
+ # Try synonym mapping of the core
66
+ dev3 = self.dev_syn.get(core, core)
67
+ candidate2 = f"{mg}{dev3}"
68
+ if candidate2 in self.allowed_devices(mg):
69
+ return candidate2
70
+
71
+ # If device lacks mg prefix but mg is present, and mg+dev is allowed, use it
72
+ if mg and not dev2.startswith(mg):
73
+ cand = f"{mg}{dev2}"
74
+ if cand in self.allowed_devices(mg):
75
+ return cand
76
+
77
+ # If already allowed, keep it
78
+ if mg and dev2 in self.allowed_devices(mg):
79
+ return dev2
80
+
81
+ # Fall back to synonym again (idempotent) or original
82
+ return dev2
83
+
84
+ def pick_default_mim(self, mg: Optional[str]) -> Optional[str]:
85
+ if mg is None:
86
+ return None
87
+ return self.mim_default_by_mg.get(mg)
88
+
89
+
90
+ def augment_prompt_with_allowlist(instruction: str, reg: TargetRegistry) -> str:
91
+ # Append a short allow-list hint (kept compact)
92
+ def bucket_preview(mg: str) -> str:
93
+ items = sorted(list(reg.allowed_devices(mg)))[:8]
94
+ return f"{mg}: {', '.join(items)}" if items else ""
95
+
96
+ hints = []
97
+ for mg in ("mg1", "mg2", "mg3", "substation", "unmapped"):
98
+ s = bucket_preview(mg)
99
+ if s:
100
+ hints.append(s)
101
+
102
+ if hints:
103
+ instruction += (
104
+ "\nAllowed device names (examples):\n"
105
+ + "\n".join(hints)
106
+ + "\nUse exactly one dot in `name`: [optional MIM].<device>.<point>\n"
107
+ )
108
+ return instruction
109
+
110
+
111
+ def _parse_name(name: str) -> Tuple[Optional[str], Optional[str], Optional[str]]:
112
+ # Accepts:
113
+ # MIM1.device.point
114
+ # device.point
115
+ # If multiple dots exist (bad), we capture best-effort pieces.
116
+ parts = name.split(".")
117
+ if len(parts) >= 3 and re.fullmatch(r"MIM[1-4]", parts[0]):
118
+ mim = parts[0]
119
+ point = parts[-1]
120
+ device = ".".join(parts[1:-1]) # may contain dots → caller will clean
121
+ return mim, device, point
122
+ elif len(parts) >= 2:
123
+ point = parts[-1]
124
+ device = ".".join(parts[:-1]) # may contain dots → caller will clean
125
+ return None, device, point
126
+ return None, None, None
127
+
128
+
129
+ def _build_name(mim: Optional[str], device: str, point: str) -> str:
130
+ if mim:
131
+ return f"{mim}.{device}.{point}"
132
+ return f"{device}.{point}"
133
+
134
+
135
+ def _is_switch_point(point: str) -> bool:
136
+ return point in {"status", "switchA", "switchB", "switchC"}
137
+
138
+
139
+ def _normalize_openclose(item: Dict[str, Any]) -> None:
140
+ op = item.get("op")
141
+ point = item.get("point")
142
+ if op in {"open", "close"} or _is_switch_point(point):
143
+ # Normalize values to uppercase OPEN/CLOSED
144
+ av = item.get("attack_value")
145
+ rv = item.get("real_value")
146
+ if isinstance(av, str):
147
+ item["attack_value"] = av.upper()
148
+ if isinstance(rv, str):
149
+ item["real_value"] = rv.upper()
150
+ # If op=open and attack_value missing → OPEN
151
+ if op == "open" and not item.get("attack_value"):
152
+ item["attack_value"] = "OPEN"
153
+ if op == "close" and not item.get("attack_value"):
154
+ item["attack_value"] = "CLOSED"
155
+ # Default real_value if absent (reasonable default)
156
+ if not item.get("real_value"):
157
+ item["real_value"] = "CLOSED" if item["attack_value"] == "OPEN" else "OPEN"
158
+
159
+
160
+ def validate_and_fix_attackplan(
161
+ ap: Dict[str, Any],
162
+ reg: TargetRegistry,
163
+ strict: bool = False,
164
+ autofix: bool = True,
165
+ cutoff: float = 0.92, # unused, kept for interface stability
166
+ ) -> Tuple[Dict[str, Any], List[str]]:
167
+ notes: List[str] = []
168
+
169
+ if not isinstance(ap, dict):
170
+ notes.append("attack plan is not a dict")
171
+ return ap, notes
172
+
173
+ # Fix top-level mim.selected using first item's scope if needed
174
+ plan: List[Dict[str, Any]] = ap.get("plan") or []
175
+ if plan:
176
+ scope0 = plan[0].get("scope", {})
177
+ mg0 = scope0.get("mg")
178
+ mim0 = scope0.get("mim")
179
+ mim0 = reg.canonicalize_mim(mim0) or reg.pick_default_mim(mg0)
180
+ # Constrain selected to a single MIM when we have one
181
+ if mim0:
182
+ ap.setdefault("mim", {})
183
+ ap["mim"]["active"] = True
184
+ ap["mim"]["selected"] = [mim0]
185
+
186
+ new_plan: List[Dict[str, Any]] = []
187
+
188
+ for it in plan:
189
+ scope = it.get("scope", {}) or {}
190
+ mg = scope.get("mg")
191
+ mim = reg.canonicalize_mim(scope.get("mim")) or reg.pick_default_mim(mg)
192
+
193
+ # Parse and canonicalize name parts
194
+ name = it.get("name", "")
195
+ mim_in, dev_raw, point_raw = _parse_name(name)
196
+ point = reg.canonicalize_point(point_raw) if point_raw else point_raw
197
+
198
+ # Choose mim priority: explicit in scope, else in name
199
+ mim_final = mim or reg.canonicalize_mim(mim_in)
200
+
201
+ # Canonicalize device using mg + synonyms
202
+ dev_final = reg.canonicalize_device(dev_raw or "", mg)
203
+
204
+ # If still unknown and strict, drop
205
+ if strict and mg and dev_final not in reg.allowed_devices(mg):
206
+ notes.append(f"dropped unknown device for mg={mg}: {dev_raw}")
207
+ continue
208
+
209
+ # Rebuild name with exactly one dot in device segment
210
+ if dev_final and point:
211
+ it["name"] = _build_name(mim_final, dev_final, point)
212
+
213
+ # Update scope.mim to the resolved one
214
+ if mim_final:
215
+ it.setdefault("scope", {})
216
+ it["scope"]["mim"] = mim_final
217
+
218
+ # Normalize switch/OPEN-CLOSED values
219
+ _normalize_openclose(it)
220
+
221
+ new_plan.append(it)
222
+
223
+ ap["plan"] = new_plan
224
+ return ap, notes
scripts/make_chat_from_plans.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ import json, argparse, uuid
3
+ from pathlib import Path
4
+
5
+ SYS = "You output ONLY JSON, no explanation. Validate against AttackPlan v1.1 semantics."
6
+
7
+ def plan_to_prompt(p):
8
+ # small, human prompt summarizing the requested edits
9
+ bits=[]
10
+ for it in p.get("plan", [])[:6]:
11
+ op = it.get("op","set"); pt = it.get("point",""); val = it.get("attack_value")
12
+ nm = it.get("name","")
13
+ scope = (it.get("scope") or {})
14
+ area = scope.get("mg"); mim = scope.get("mim")
15
+ s = f"{op} {pt} on {nm} to {val}"
16
+ if mim: s += f" in {mim}"
17
+ if area: s += f" ({area})"
18
+ bits.append(s)
19
+ return "; ".join(bits) if bits else "Generate an AttackPlan JSON v1.1 for no-op."
20
+
21
+ def main():
22
+ ap = argparse.ArgumentParser()
23
+ ap.add_argument("--src", required=True, help="scripts/train_attackplan.aug.jsonl (or filtered)")
24
+ ap.add_argument("--out", default="datasets/chat_attackplan.jsonl")
25
+ args = ap.parse_args()
26
+
27
+ src = Path(args.src).read_text(encoding="utf-8-sig").splitlines()
28
+ out = []
29
+ for ln in src:
30
+ if not ln.strip(): continue
31
+ obj = json.loads(ln)
32
+ prompt = plan_to_prompt(obj)
33
+ out.append({
34
+ "id": str(uuid.uuid4()),
35
+ "messages": [
36
+ {"role":"system","content":SYS},
37
+ {"role":"user","content":f"Task: {prompt}\nReturn ONLY the JSON."},
38
+ {"role":"assistant","content": json.dumps(obj, ensure_ascii=False)}
39
+ ]
40
+ })
41
+ Path(args.out).parent.mkdir(parents=True, exist_ok=True)
42
+ Path(args.out).write_text("\n".join(json.dumps(x, ensure_ascii=False) for x in out)+"\n", encoding="utf-8")
43
+ print("[ok] wrote", Path(args.out).resolve(), "rows:", len(out))
44
+
45
+ if __name__ == "__main__":
46
+ main()
scripts/make_property_glossary.py ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ make_property_glossary.py
4
+ Summarize properties from AttackPlan JSONL and merge semantics from kb/specs/point_semantics.json.
5
+
6
+ Usage:
7
+ python scripts/make_property_glossary.py \
8
+ --src scripts/train_attackplan.filtered.jsonl \
9
+ --sem kb/specs/point_semantics.json
10
+ """
11
+ from __future__ import annotations
12
+ import argparse, json, statistics
13
+ from pathlib import Path
14
+ from collections import Counter, defaultdict
15
+
16
+ def load_plans(src: Path):
17
+ raw = src.read_text(encoding="utf-8-sig").splitlines()
18
+ plans = []
19
+ for ln in raw:
20
+ s = ln.strip()
21
+ if not s: continue
22
+ try:
23
+ obj = json.loads(s)
24
+ except Exception:
25
+ continue
26
+ if isinstance(obj, dict) and isinstance(obj.get("plan"), list):
27
+ plans.append(obj)
28
+ return plans
29
+
30
+ def coerce_num(x):
31
+ try: return float(str(x))
32
+ except Exception: return None
33
+
34
+ def main():
35
+ ap = argparse.ArgumentParser()
36
+ ap.add_argument("--src", required=True, help="AttackPlan JSONL (filtered)")
37
+ ap.add_argument("--sem", required=True, help="Semantics JSON (kb/specs/point_semantics.json)")
38
+ args = ap.parse_args()
39
+
40
+ src = Path(args.src)
41
+ sem = json.loads(Path(args.sem).read_text(encoding="utf-8"))
42
+ props_sem = sem.get("properties", {})
43
+
44
+ plans = load_plans(src)
45
+ if not plans:
46
+ raise SystemExit(f"No AttackPlan rows in {src}")
47
+
48
+ freq = Counter()
49
+ vals = defaultdict(list)
50
+ ops = Counter()
51
+ applys = Counter()
52
+
53
+ for plan in plans:
54
+ for it in plan.get("plan", []):
55
+ p = it.get("point","")
56
+ freq[p]+=1
57
+ ops[it.get("op","set")] += 1
58
+ applys[(it.get("scope") or {}).get("apply","both")] += 1
59
+ v = it.get("attack_value", None)
60
+ if v is not None: vals[p].append(v)
61
+
62
+ lines = []
63
+ lines.append("# Property Glossary (auto-generated)\n")
64
+ lines.append("**REALITY FILTER:** Items reflect your filtered dataset and semantics. If a property has low confidence in the semantics file, verify before relying on it.\n")
65
+ lines.append(f"- Source file: `{src}`\n")
66
+ lines.append(f"- Total plans: {len(plans)}\n")
67
+ lines.append(f"- Operation distribution: {dict(ops)}\n")
68
+ lines.append(f"- Scope.apply distribution: {dict(applys)}\n")
69
+ lines.append("\n---\n## Properties\n")
70
+
71
+ for prop, count in freq.most_common():
72
+ semp = props_sem.get(prop, {})
73
+ unit = semp.get("unit", "[n/a]")
74
+ conf = semp.get("confidence", "unknown")
75
+ notes = semp.get("notes", "")
76
+ # example values
77
+ ex_vals = vals[prop][:6]
78
+ nums = [coerce_num(v) for v in vals[prop]]
79
+ nums = [n for n in nums if n is not None]
80
+ if nums:
81
+ stats = f"min={min(nums):.3f}, p50={statistics.median(nums):.3f}, max={max(nums):.3f}"
82
+ else:
83
+ stats = "[n/a]"
84
+ # confidence label
85
+ conf_lbl = {"high":"[Verified]","medium":"[Inference]","low":"[Unverified]"}.get(conf, "[Unverified]")
86
+ # write section
87
+ lines.append(f"### `{prop}` \n- **count:** {count} \n- **unit:** {unit} \n- **confidence:** {conf_lbl} \n- **notes:** {notes or '[n/a]'} \n- **examples:** {ex_vals} \n- **numeric stats:** {stats}\n")
88
+
89
+ out = Path("kb/cheatsheets/property_glossary.md")
90
+ out.parent.mkdir(parents=True, exist_ok=True)
91
+ out.write_text("\n".join(lines), encoding="utf-8")
92
+ print("[ok] wrote", out.resolve())
93
+
94
+ if __name__ == "__main__":
95
+ main()
scripts/run_hybrid_infer.py ADDED
@@ -0,0 +1,340 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ import argparse, json, os, re, sys, time, random
3
+ from pathlib import Path
4
+
5
+ import torch
6
+ from transformers import AutoModelForCausalLM, AutoTokenizer
7
+ from peft import PeftModel
8
+ from jsonschema import Draft7Validator
9
+
10
+ # ---------------------------
11
+ # RAG (optional)
12
+ # ---------------------------
13
+ def try_load_rag(index_path: str, kb_root: str):
14
+ if not index_path:
15
+ return None
16
+ try:
17
+ import faiss
18
+ from sentence_transformers import SentenceTransformer
19
+ except Exception:
20
+ print("[rag] sentence-transformers / faiss not available, skipping RAG.")
21
+ return None
22
+ if not os.path.exists(index_path):
23
+ print(f"[rag] index not found at {index_path}, skipping.")
24
+ return None
25
+ model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
26
+ index = faiss.read_index(index_path)
27
+ return {"enc": model, "index": index, "kb_root": kb_root or ""}
28
+
29
+ def rag_retrieve(rag, query: str, k: int):
30
+ if not rag:
31
+ return []
32
+ import numpy as np
33
+ vec = rag["enc"].encode([query])
34
+ D, I = rag["index"].search(vec, k)
35
+ I = I[0].tolist()
36
+ # If you have a mapping file alongside the index, use it; else emit stubs.
37
+ mapping_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "rag_index", "kb.mapping.json")
38
+ out = []
39
+ if os.path.exists(mapping_path):
40
+ try:
41
+ with open(mapping_path, "r", encoding="utf-8") as f:
42
+ mapping = json.load(f)
43
+ for i in I:
44
+ s = mapping.get(str(i))
45
+ out.append(s if s else f"[KB#{i}]")
46
+ except Exception:
47
+ out = [f"[KB#{i}]" for i in I]
48
+ else:
49
+ out = [f"[KB#{i}]" for i in I]
50
+ return out
51
+
52
+ # ---------------------------
53
+ # Schema helpers
54
+ # ---------------------------
55
+ def load_schema(path: str):
56
+ if not path:
57
+ return None
58
+ with open(path, "r", encoding="utf-8") as f:
59
+ return json.load(f)
60
+
61
+ def validate_schema(obj, schema):
62
+ if not schema:
63
+ return True, []
64
+ v = Draft7Validator(schema)
65
+ errs = sorted(v.iter_errors(obj), key=lambda e: e.path)
66
+ if errs:
67
+ msgs = []
68
+ for e in errs:
69
+ loc = ".".join([str(p) for p in e.path])
70
+ msgs.append(f"{loc or '<root>'}: {e.message}")
71
+ return False, msgs
72
+ return True, []
73
+
74
+ # ---------------------------
75
+ # Guard (allow list + coerce)
76
+ # ---------------------------
77
+ def _load_targets(path: str):
78
+ if not path:
79
+ return {"allow": [], "allow_prefix": [], "coerce": {}}
80
+ with open(path, "r", encoding="utf-8") as f:
81
+ data = json.load(f)
82
+ allow = list(dict.fromkeys(data.get("allow", [])))
83
+ allow_prefix = list(dict.fromkeys(data.get("allow_prefix", [])))
84
+ coerce = {str(k).lower(): v for k, v in data.get("coerce", {}).items()}
85
+ print(f"[guard] strict target list loaded: {len(allow)} names, {len(allow_prefix)} prefixes")
86
+ return {"allow": allow, "allow_prefix": allow_prefix, "coerce": coerce}
87
+
88
+ def _base_name(full: str) -> str:
89
+ parts = full.split(".", 1)
90
+ if len(parts) == 2 and parts[0].startswith("MIM") and parts[0][3:].isdigit():
91
+ return parts[1]
92
+ return full
93
+
94
+ def _is_allowed(name: str, allow: list, allow_prefix: list) -> bool:
95
+ bn = _base_name(name)
96
+ if name in allow or bn in allow:
97
+ return True
98
+ for p in allow_prefix:
99
+ if name.startswith(p) or bn.startswith(p):
100
+ return True
101
+ return False
102
+
103
+ def _coerce_value(val, coerce_map: dict):
104
+ if isinstance(val, str):
105
+ key = val.strip().lower()
106
+ if key in coerce_map:
107
+ return coerce_map[key]
108
+ if isinstance(val, bool):
109
+ return "OPEN" if val else "CLOSED"
110
+ return val
111
+
112
+ def sanitize_step_scope(step: dict):
113
+ # Fix common schema issues for scope.mim
114
+ name = step.get("name", "")
115
+ sc = step.get("scope", {}) or {}
116
+ mim = sc.get("mim", None)
117
+ if isinstance(mim, bool):
118
+ mim = None
119
+ if mim is None and isinstance(name, str) and name.startswith("MIM"):
120
+ m = re.match(r"^(MIM[1-4])\.", name)
121
+ if m: mim = m.group(1)
122
+ sc["mim"] = mim
123
+ step["scope"] = sc
124
+ return step
125
+
126
+ def guard_and_coerce(plan: list, targets: dict, strict: bool) -> list:
127
+ allow = targets.get("allow", [])
128
+ allow_prefix = targets.get("allow_prefix", [])
129
+ coerce_map = targets.get("coerce", {})
130
+ kept, rejects = [], []
131
+ for i, step in enumerate(plan or []):
132
+ name = step.get("name", "")
133
+ step = sanitize_step_scope(step)
134
+ ok = _is_allowed(name, allow, allow_prefix)
135
+ if not ok and strict:
136
+ rejects.append((i, name, "not_in_allowlist"))
137
+ continue
138
+ step["attack_value"] = _coerce_value(step.get("attack_value"), coerce_map)
139
+ kept.append(step)
140
+ if rejects:
141
+ for i, n, why in rejects:
142
+ print(f"[reject {i}] guard: {n} -> {why}")
143
+ if not kept:
144
+ raise ValueError("All steps rejected by guard.")
145
+ return kept
146
+
147
+ # ---------------------------
148
+ # Prompting & extraction
149
+ # ---------------------------
150
+ def build_prompt(task: str, user_prompt: str, rag_snippets=None, must_names=None):
151
+ assert task in {"attackplan", "chat"}
152
+ parts = []
153
+ if task == "attackplan":
154
+ parts.append("You output only AttackPlan v1.1 JSON. No other text.")
155
+ if rag_snippets:
156
+ parts.append("\nContext")
157
+ for snip in rag_snippets:
158
+ parts.append(snip)
159
+ if must_names:
160
+ parts.append("\nHard constraints:")
161
+ parts.append(" - Your plan MUST include the following names (exact match) at least once:")
162
+ for n in must_names:
163
+ parts.append(f" * {n}")
164
+ parts.append("\nInstruction")
165
+ parts.append(user_prompt.strip())
166
+ parts.append("\nKeys: version, time, mim, plan\n### Response:")
167
+ else:
168
+ parts.append(user_prompt.strip())
169
+ return "\n".join(parts)
170
+
171
+ def extract_first_json(text: str):
172
+ # Try code fences first
173
+ fence = re.search(r"```(?:json)?\s*(\{.*?\})\s*```", text, re.S | re.I)
174
+ if fence:
175
+ try:
176
+ return json.loads(fence.group(1))
177
+ except Exception:
178
+ pass
179
+ # Brace scanner (no recursive regex)
180
+ stack = []
181
+ start = None
182
+ for i, ch in enumerate(text):
183
+ if ch == "{":
184
+ if not stack:
185
+ start = i
186
+ stack.append("{")
187
+ elif ch == "}":
188
+ if stack:
189
+ stack.pop()
190
+ if not stack and start is not None:
191
+ candidate = text[start:i+1]
192
+ try:
193
+ return json.loads(candidate)
194
+ except Exception:
195
+ start = None # keep scanning
196
+ raise ValueError("No JSON object found in model output.")
197
+
198
+ def plan_contains_all_names(plan: list, must_names: list) -> bool:
199
+ names = {step.get("name", "") for step in (plan or [])}
200
+ return all(n in names for n in must_names)
201
+
202
+ # ---------------------------
203
+ # Model loading / generation
204
+ # ---------------------------
205
+ def load_model_and_tokenizer(base: str, adapter: str, local_files_only: bool):
206
+ tok = AutoTokenizer.from_pretrained(base, use_fast=True, local_files_only=local_files_only)
207
+ if tok.pad_token is None:
208
+ tok.pad_token = tok.eos_token
209
+ model = AutoModelForCausalLM.from_pretrained(
210
+ base,
211
+ torch_dtype=torch.bfloat16 if torch.cuda.is_available() else torch.float32,
212
+ device_map="auto",
213
+ local_files_only=local_files_only,
214
+ )
215
+ if adapter:
216
+ model = PeftModel.from_pretrained(model, adapter, local_files_only=local_files_only)
217
+ print(f"[model] Loaded PEFT adapter: {adapter}")
218
+ return model, tok
219
+
220
+ def generate_json(model, tok, prompt, max_new_tokens=320, temp=0.2, top_p=0.9):
221
+ inputs = tok(prompt, return_tensors="pt", padding=True, truncation=True).to(model.device)
222
+ with torch.no_grad():
223
+ out = model.generate(
224
+ **inputs,
225
+ max_new_tokens=int(max_new_tokens),
226
+ do_sample=True,
227
+ temperature=float(temp),
228
+ top_p=float(top_p),
229
+ pad_token_id=tok.pad_token_id,
230
+ eos_token_id=tok.eos_token_id,
231
+ )
232
+ text = tok.decode(out[0], skip_special_tokens=True)
233
+ return text
234
+
235
+ # ---------------------------
236
+ # Main
237
+ # ---------------------------
238
+ def main():
239
+ ap = argparse.ArgumentParser()
240
+ ap.add_argument("--base", required=True, help="HF model id or path")
241
+ ap.add_argument("--adapter", default="", help="PEFT adapter path")
242
+ ap.add_argument("--task", choices=["attackplan", "chat"], default="attackplan")
243
+ ap.add_argument("--schema", default="", help="JSON schema for validation")
244
+ ap.add_argument("--prompt", required=True, help="User instruction")
245
+ ap.add_argument("--rag_index", default="", help="FAISS index path")
246
+ ap.add_argument("--kb_root", default="", help="KB root for RAG (optional)")
247
+ ap.add_argument("--idx", type=int, default=4, help="RAG top-k")
248
+ ap.add_argument("--targets", default="", help="allowed_targets.json")
249
+ ap.add_argument("--strict_targets", action="store_true")
250
+ ap.add_argument("--must_names", default="", help="Comma separated exact names that MUST appear")
251
+ ap.add_argument("--max_attempts", type=int, default=3)
252
+ ap.add_argument("--temp", type=float, default=0.2)
253
+ ap.add_argument("--top_p", type=float, default=0.9)
254
+ ap.add_argument("--max_new_tokens", type=int, default=320)
255
+ ap.add_argument("--seed", type=int, default=42)
256
+ ap.add_argument("--save", default="", help="Optional path to save final JSON")
257
+ ap.add_argument("--offline", action="store_true", help="Force offline (no HF Hub network)")
258
+ args = ap.parse_args()
259
+
260
+ # Environment hardening: never push, optional offline
261
+ os.environ.setdefault("HF_HUB_DISABLE_TELEMETRY", "1")
262
+ os.environ.setdefault("HF_HUB_DISABLE_PROGRESS_BARS", "1")
263
+ if args.offline:
264
+ os.environ["HF_HUB_OFFLINE"] = "1"
265
+
266
+ random.seed(args.seed)
267
+ torch.manual_seed(args.seed)
268
+
269
+ # Load components
270
+ schema = load_schema(args.schema)
271
+ targets = _load_targets(args.targets) if args.targets else {"allow": [], "allow_prefix": [], "coerce": {}}
272
+ rag = try_load_rag(args.rag_index, args.kb_root)
273
+ model, tok = load_model_and_tokenizer(args.base, args.adapter, local_files_only=args.offline)
274
+
275
+ must_names = [s.strip() for s in args.must_names.split(",") if s.strip()] if args.must_names else []
276
+
277
+ attempts = 0
278
+ last_errs = []
279
+ final = None
280
+
281
+ while attempts < max(1, args.max_attempts):
282
+ attempts += 1
283
+ snippets = rag_retrieve(rag, args.prompt, args.idx) if rag else None
284
+ prompt_text = build_prompt(args.task, args.prompt, snippets, must_names)
285
+
286
+ raw = generate_json(model, tok, prompt_text, args.max_new_tokens, args.temp, args.top_p)
287
+ try:
288
+ obj = extract_first_json(raw)
289
+ except Exception as e:
290
+ print(f"[attempt {attempts}] parse_error: {e}")
291
+ last_errs.append(f"parse_error: {e}")
292
+ continue
293
+
294
+ if args.task == "attackplan":
295
+ if "plan" not in obj or not isinstance(obj["plan"], list):
296
+ print(f"[attempt {attempts}] schema_error: plan missing or not a list")
297
+ last_errs.append("schema: plan missing")
298
+ continue
299
+
300
+ try:
301
+ obj["plan"] = guard_and_coerce(obj["plan"], targets, strict=args.strict_targets)
302
+ except Exception as e:
303
+ print(f"[attempt {attempts}] guard_reject: {e}")
304
+ last_errs.append(f"guard_reject: {e}")
305
+ continue
306
+
307
+ if must_names and not plan_contains_all_names(obj["plan"], must_names):
308
+ print(f"[attempt {attempts}] must_names not satisfied, retrying…")
309
+ last_errs.append("must_names")
310
+ continue
311
+
312
+ ok, errs = validate_schema(obj, schema)
313
+ if not ok:
314
+ for i, m in enumerate(errs[:5], 1):
315
+ print(f"[attempt {attempts}] schema_error {i}: {m}")
316
+ last_errs.extend(errs)
317
+ continue
318
+
319
+ final = obj
320
+ break
321
+
322
+ if final is None:
323
+ print("[error] failed to produce a valid plan.")
324
+ if last_errs:
325
+ print("Last errors:")
326
+ for m in last_errs[-10:]:
327
+ print(" -", m)
328
+ sys.exit(2)
329
+
330
+ out_text = json.dumps(final, ensure_ascii=False)
331
+ print(out_text)
332
+
333
+ if args.save:
334
+ Path(os.path.dirname(args.save) or ".").mkdir(parents=True, exist_ok=True)
335
+ with open(args.save, "w", encoding="utf-8") as f:
336
+ f.write(out_text)
337
+ print(f"[ok] saved to {args.save}")
338
+
339
+ if __name__ == "__main__":
340
+ main()
scripts/seed_kb_examples.py ADDED
@@ -0,0 +1,256 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ seed_kb_examples.py
4
+ Create prompt→AttackPlan examples for RAG from train_attackplan.jsonl
5
+
6
+ Usage (from repo root):
7
+ %run scripts/seed_kb_examples.py
8
+ # or choose a different source / count
9
+ %run scripts/seed_kb_examples.py --src scripts/train_attackplan.jsonl --k 40
10
+ """
11
+
12
+ from __future__ import annotations
13
+ import argparse, json, re, random
14
+ from pathlib import Path
15
+ from typing import Dict, Any, List, Tuple
16
+
17
+ # ----------------------
18
+ # Helpers
19
+ # ----------------------
20
+
21
+ def load_plans(src: Path) -> List[Dict[str, Any]]:
22
+ lines = src.read_text(encoding="utf-8").splitlines()
23
+ out = []
24
+ for ln in lines:
25
+ ln = ln.strip()
26
+ if not ln:
27
+ continue
28
+ try:
29
+ obj = json.loads(ln)
30
+ # tolerate files that contain chat rows by accident
31
+ if isinstance(obj, dict) and "plan" in obj and isinstance(obj["plan"], list):
32
+ out.append(obj)
33
+ except Exception:
34
+ continue
35
+ return out
36
+
37
+ def infer_device_name(item_name: str) -> str:
38
+ # item_name may be:
39
+ # "MIM2.mg1microgrid_switch2.status" or "mg1load_41.constant_power_A"
40
+ # Take the middle chunk if MIM is present, else first chunk before '.'
41
+ parts = item_name.split(".")
42
+ if parts[0].startswith("MIM") and len(parts) >= 3:
43
+ return parts[1]
44
+ return parts[0]
45
+
46
+ def infer_device_type(dev: str) -> str:
47
+ s = dev.lower()
48
+ if "switch" in s: return "switch"
49
+ if "inverter" in s: return "inverter"
50
+ if "diesel" in s or re.search(r"\bgen|generator\b", s): return "generator"
51
+ if "capacitor" in s or s.startswith("cap_"): return "capacitor"
52
+ if "regulator" in s or s.startswith("reg_"): return "regulator"
53
+ if "load" in s: return "load"
54
+ return "other"
55
+
56
+ def collect_tags(plan: Dict[str, Any]) -> Dict[str, List[str]]:
57
+ ops, points, mims, applys, dtypes = set(), set(), set(), set(), set()
58
+ for it in plan.get("plan", []):
59
+ ops.add(it.get("op", "set"))
60
+ points.add(it.get("point", ""))
61
+ sc = it.get("scope") or {}
62
+ ap = sc.get("apply", "both")
63
+ applys.add(ap)
64
+ mim = sc.get("mim")
65
+ if mim: mims.add(mim)
66
+ dev = infer_device_name(it.get("name", ""))
67
+ dtypes.add(infer_device_type(dev))
68
+ return {
69
+ "ops": sorted(x for x in ops if x),
70
+ "points": sorted(x for x in points if x),
71
+ "apply": sorted(x for x in applys if x),
72
+ "mims": sorted(mims),
73
+ "device_types": sorted(dtypes),
74
+ }
75
+
76
+ def item_to_phrase(it: Dict[str, Any]) -> str:
77
+ # Generate a concise, human prompt fragment for RAG.
78
+ op = it.get("op", "set")
79
+ point = it.get("point", "")
80
+ val = it.get("attack_value", "")
81
+ nm = infer_device_name(it.get("name", ""))
82
+ sc = it.get("scope") or {}
83
+ mim = sc.get("mim")
84
+ # Normalize value strings a bit
85
+ sval = str(val)
86
+ if isinstance(val, float) and sval.endswith(".0"):
87
+ sval = sval[:-2]
88
+ # Choose verb template
89
+ if op in {"open","close","trip"}:
90
+ base = f"{op} {infer_device_type(nm)} {nm}"
91
+ elif op in {"increase","decrease","scale"}:
92
+ base = f"{op} {point} of {nm} by {sval}"
93
+ else: # set/default
94
+ base = f"set {point} of {nm} to {sval}"
95
+ if mim:
96
+ base += f" in {mim}"
97
+ return base
98
+
99
+ def plan_to_prompt(plan: Dict[str, Any], max_items: int = 6) -> str:
100
+ items = plan.get("plan", [])[:max_items]
101
+ if not items:
102
+ return "Generate an AttackPlan JSON v1.1 (no items)."
103
+ phrases = [item_to_phrase(it) for it in items]
104
+ if len(phrases) == 1:
105
+ return phrases[0]
106
+ return "; ".join(phrases)
107
+
108
+ def score(plan: Dict[str, Any]) -> Tuple[int,int,int,int]:
109
+ """Sort key to promote diversity: favor both/apply, more mims, more ops, more device types."""
110
+ tags = collect_tags(plan)
111
+ return (
112
+ 1 if "both" in tags["apply"] else 0,
113
+ len(tags["mims"]),
114
+ len(tags["ops"]),
115
+ len(tags["device_types"]),
116
+ )
117
+
118
+ def pick_diverse(plans: List[Dict[str, Any]], k: int, seed: int = 7) -> List[Dict[str, Any]]:
119
+ rng = random.Random(seed)
120
+ # Shuffle then sort by our diversity score (descending)
121
+ rng.shuffle(plans)
122
+ plans.sort(key=score, reverse=True)
123
+ # Simple greedy: walk and enforce bucketing caps so we cover ops/apply/points
124
+ seen_keys = set()
125
+ picked = []
126
+ buckets = {}
127
+ caps = {
128
+ "apply:glm_only": max(1, k//6),
129
+ "apply:both": max(1, k//3),
130
+ }
131
+ for p in plans:
132
+ tags = collect_tags(p)
133
+ key_apply = f"apply:{'glm_only' if 'glm_only' in tags['apply'] else 'both'}"
134
+ buckets.setdefault(key_apply, 0)
135
+ if buckets[key_apply] >= caps[key_apply]:
136
+ continue
137
+ # de-dup by items signature
138
+ sig = tuple((it.get("op"), it.get("point"), (it.get("scope") or {}).get("mim")) for it in p.get("plan", [])[:4])
139
+ if sig in seen_keys:
140
+ continue
141
+ seen_keys.add(sig)
142
+ picked.append(p)
143
+ buckets[key_apply] += 1
144
+ if len(picked) >= k:
145
+ break
146
+ # If still short, top up ignoring caps
147
+ i = 0
148
+ while len(picked) < k and i < len(plans):
149
+ if plans[i] not in picked:
150
+ picked.append(plans[i])
151
+ i += 1
152
+ return picked[:k]
153
+
154
+ def write_examples(plans: List[Dict[str, Any]], outdir: Path):
155
+ outdir.mkdir(parents=True, exist_ok=True)
156
+ for i, p in enumerate(plans, 1):
157
+ ex = {
158
+ "prompt": plan_to_prompt(p),
159
+ "attack_plan": p,
160
+ "tags": collect_tags(p)
161
+ }
162
+ Path(outdir, f"ex-{i:04d}.json").write_text(json.dumps(ex, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
163
+
164
+ def write_canonical_snippets(outdir: Path):
165
+ """A couple of tiny single-item plans as structural references."""
166
+ outdir.mkdir(parents=True, exist_ok=True)
167
+ mini = [
168
+ {
169
+ "title": "set_inverter_Pref",
170
+ "plan": {
171
+ "version": "1.1",
172
+ "time": {"start_s": 0, "end_s": 30},
173
+ "mim": {"active": True, "selected": ["MIM2"]},
174
+ "plan": [{
175
+ "name": "MIM2.mg1inverter_XXX.Pref",
176
+ "scope": {"mg": "mg1", "mim":"MIM2", "apply":"both"},
177
+ "op": "set", "point": "Pref", "attack_value": 10000, "real_value": 0,
178
+ "phase": None, "window": {"point_start_s": 1, "point_stop_s": 20}
179
+ }]
180
+ }
181
+ },
182
+ {
183
+ "title": "open_switch_status",
184
+ "plan": {
185
+ "version": "1.1",
186
+ "time": {"start_s": 0, "end_s": 30},
187
+ "mim": {"active": True, "selected": ["MIM1"]},
188
+ "plan": [{
189
+ "name": "MIM1.mg2microgrid_switch_YYY.status",
190
+ "scope": {"mg": "mg2", "mim":"MIM1", "apply":"both"},
191
+ "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED",
192
+ "phase": None, "window": {"point_start_s": 2, "point_stop_s": 10}
193
+ }]
194
+ }
195
+ },
196
+ {
197
+ "title": "glm_only_unmapped_load",
198
+ "plan": {
199
+ "version": "1.1",
200
+ "time": {"start_s": 0, "end_s": 30},
201
+ "mim": {"active": True, "selected": ["MIM3"]},
202
+ "plan": [{
203
+ "name": "load_42.constant_power_A",
204
+ "scope": {"mg": "unmapped", "mim": None, "apply":"glm_only"},
205
+ "op": "set", "point": "constant_power_A", "attack_value": 25000, "real_value": 20000,
206
+ "phase": None, "window": {"point_start_s": 5, "point_stop_s": 25}
207
+ }]
208
+ }
209
+ }
210
+ ]
211
+ for m in mini:
212
+ Path(outdir, f"{m['title']}.json").write_text(json.dumps(m["plan"], ensure_ascii=False, indent=2)+"\n", encoding="utf-8")
213
+
214
+ def main():
215
+ ap = argparse.ArgumentParser()
216
+ ap.add_argument("--src", type=str, default="scripts/train_attackplan.jsonl",
217
+ help="Path to your AttackPlan JSONL")
218
+ ap.add_argument("--out", type=str, default="kb/examples",
219
+ help="Output folder for RAG examples")
220
+ ap.add_argument("--k", type=int, default=40,
221
+ help="How many examples to write")
222
+ ap.add_argument("--seed", type=int, default=7)
223
+ ap.add_argument("--write_snippets", action="store_true",
224
+ help="Also write a few canonical mini-plans to kb/snippets/json/")
225
+ args = ap.parse_args()
226
+
227
+ src = Path(args.src)
228
+ if not src.exists():
229
+ # Try a couple of common alternate locations
230
+ candidates = [
231
+ Path("..") / "EditGlm" / "scripts" / "train_attackplan.jsonl",
232
+ Path("scripts") / "train_attackplan.jsonl"
233
+ ]
234
+ for c in candidates:
235
+ if c.exists():
236
+ src = c; break
237
+
238
+ print("[seed] reading", src.resolve())
239
+ plans = load_plans(src)
240
+ if not plans:
241
+ raise SystemExit("No valid plans found in JSONL.")
242
+
243
+ picked = pick_diverse(plans, k=args.k, seed=args.seed)
244
+ write_examples(picked, Path(args.out))
245
+
246
+ if args.write_snippets:
247
+ write_canonical_snippets(Path("kb/snippets/json"))
248
+
249
+ print(f"[seed] wrote {len(picked)} examples to {Path(args.out).resolve()}")
250
+ if args.write_snippets:
251
+ print(f"[seed] wrote canonical mini snippets to {Path('kb/snippets/json').resolve()}")
252
+
253
+
254
+
255
+ if __name__ == "__main__":
256
+ main()
scripts/split_attackplan_jsonl.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ split_attackplan_jsonl.py
4
+ Shuffle and split AttackPlan JSONL into datasets/train|val|test.jsonl
5
+
6
+ Usage:
7
+ %run scripts/split_attackplan_jsonl.py --src "C:/Users/adetu/Dropbox/Ire_Research/my_code/scripts/train_attackplan.jsonl"
8
+ """
9
+ from __future__ import annotations
10
+ import argparse, json, random, hashlib
11
+ from pathlib import Path
12
+
13
+ def plan_sig(plan: dict) -> str:
14
+ # crude signature to detect duplicates across splits
15
+ blob = json.dumps(plan.get("plan", []), sort_keys=True)
16
+ return hashlib.md5(blob.encode("utf-8")).hexdigest()
17
+
18
+ def main():
19
+ ap = argparse.ArgumentParser()
20
+ ap.add_argument("--src", type=str, required=True)
21
+ ap.add_argument("--seed", type=int, default=7)
22
+ ap.add_argument("--train", type=float, default=0.70)
23
+ ap.add_argument("--val", type=float, default=0.15)
24
+ args = ap.parse_args()
25
+
26
+ src = Path(args.src)
27
+ lines = [ln for ln in src.read_text(encoding="utf-8-sig").splitlines() if ln.strip()]
28
+ random.Random(args.seed).shuffle(lines)
29
+
30
+ n = len(lines)
31
+ ntr = int(args.train * n)
32
+ nv = int(args.val * n)
33
+ test_start = ntr + nv
34
+
35
+ outdir = Path("datasets"); outdir.mkdir(exist_ok=True)
36
+ Path(outdir, "train.jsonl").write_text("\n".join(lines[:ntr]) + "\n", encoding="utf-8")
37
+ Path(outdir, "val.jsonl").write_text("\n".join(lines[ntr:test_start]) + "\n", encoding="utf-8")
38
+ Path(outdir, "test.jsonl").write_text("\n".join(lines[test_start:]) + "\n", encoding="utf-8")
39
+
40
+ # duplicate signature report
41
+ import json as _json
42
+ buckets = {"train":[], "val":[], "test":[]}
43
+ for name, chunk in [("train", lines[:ntr]), ("val", lines[ntr:test_start]), ("test", lines[test_start:])]:
44
+ for ln in chunk:
45
+ try:
46
+ obj = _json.loads(ln)
47
+ if isinstance(obj, dict) and "plan" in obj:
48
+ buckets[name].append(plan_sig(obj))
49
+ except Exception:
50
+ pass
51
+ inter = set(buckets["train"]) & set(buckets["val"]) | set(buckets["train"]) & set(buckets["test"]) | set(buckets["val"]) & set(buckets["test"])
52
+ print(f"[done] train/val/test = {ntr}/{nv}/{n - ntr - nv}. duplicate_plans_across_splits={len(inter)}")
53
+
54
+ if __name__ == "__main__":
55
+ main()
scripts/train_attackplan.aug.jsonl ADDED
The diff for this file is too large to render. See raw diff
 
scripts/train_attackplan.filtered.jsonl ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_1.constant_power_A", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 32953.311, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_1.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 26033.967, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_1.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 46037.379, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_2.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 11448.726, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_4.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 41435.28, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
2
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_7.constant_power_A", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 20148.715, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_7.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 10749.913, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_7.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 18672.914, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
3
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_9.constant_power_A", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 22794.217, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_12.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 26537.042, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_16.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 24952.078, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
4
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_17.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 14464.779, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_19.constant_power_A", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 45097.329, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_24.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 35867.219, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
5
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_30.constant_power_A", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 32339.273, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_30.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 52645.054, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_30.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 27229.055, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_31.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 21632.003, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
6
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_32.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 22778.269, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_35.constant_power_A", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 22511.559, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
7
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM1.mg1load_39.constant_power_B", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 23607.999, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_41.constant_power_C", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 18551.846, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_42.constant_power_A", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 16282.943, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_42.constant_power_B", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 21711.237, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
8
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM1.mg1load_42.constant_power_C", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 19063.688, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_46.constant_power_A", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 23979.889, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
9
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM1.mg1load_49.constant_power_A", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 51806.12, "real_value": 35000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_49.constant_power_B", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 43264.604, "real_value": 70000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_49.constant_power_C", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 32134.299, "real_value": 35000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_50.constant_power_A", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 50285.637, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_50.constant_power_B", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 26079.381, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
10
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM1.mg1load_50.constant_power_C", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 39558.524, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_51.constant_power_A", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 10784.145, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_51.constant_power_B", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 23364.317, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1load_51.constant_power_C", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 25291.417, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_52.constant_power_A", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 42921.038, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
11
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_52.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 55019.112, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_52.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 32549.901, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_53.constant_power_A", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 47811.815, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_53.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 43774.795, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_53.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 43195.808, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
12
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_56.constant_power_A", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 19481.967, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_56.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 23283.044, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
13
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_56.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 11213.389, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_59.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 22942.577, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_60.constant_power_A", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 29861.919, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_60.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 26438.496, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
14
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_60.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 15691.911, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_63.constant_power_A", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 38467.811, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
15
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_63.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 26721.935, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_63.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 24683.832, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
16
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_66.constant_power_A", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 43543.598, "real_value": 75000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationload_66.constant_power_B", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 71189.055, "real_value": 75000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
17
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationload_66.constant_power_C", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 78707.993, "real_value": 75000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_68.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 27667.677, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_69.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 52771.194, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_70.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 27279.689, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_71.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 31136.843, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
18
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3load_73.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 36611.861, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_75.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 55367.713, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
19
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3load_761.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 14639.137, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_77.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 29333.443, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_77.constant_power_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 39398.509, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_77.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 43564.94, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
20
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3load_80.constant_power_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 17385.071, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_82.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 10663.412, "real_value": 10000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
21
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3load_83.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 29061.959, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_84.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 23809.873, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_85.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 40619.657, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_86.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 22351.855, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_86.constant_power_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 23524.002, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
22
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3load_86.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 11079.858, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_87.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 55981.32, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_87.constant_power_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 51198.78, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_87.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 54980.527, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_88.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 51914.925, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
23
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3load_92.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 35959.153, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_94.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 24141.484, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_95.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 22685.791, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_95.constant_power_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 11244.956, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
24
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3load_95.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 11346.952, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_96.constant_power_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 14175.264, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_98.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 26492.128, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_98.constant_power_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 33602.146, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_98.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 22103.024, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
25
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3load_99.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 20009.331, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_99.constant_power_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 26050.597, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_99.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 24058.575, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_100.constant_power_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 8.636, "real_value": 10.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
26
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3load_100.constant_power_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 13.743, "real_value": 10.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3load_100.constant_power_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 6.486, "real_value": 10.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM2.mg2load_102.constant_power_C", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 16947.791, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
27
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM2.mg2load_103.constant_power_C", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 34566.538, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM2.mg2load_104.constant_power_C", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "constant_power_C", "attack_value": 24913.689, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM2.mg2load_106.constant_power_B", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 53957.477, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM2.mg2load_107.constant_power_B", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "constant_power_B", "attack_value": 59724.109, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM2.mg2load_109.constant_power_A", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 38639.578, "real_value": 40000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
28
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM2.mg2load_111.constant_power_A", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 19676.693, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM2.mg2load_114.constant_power_A", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "constant_power_A", "attack_value": 16852.717, "real_value": 20000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
29
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "unmappedGen1.power_out_A", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "power_out_A", "attack_value": "30000+5000j", "real_value": "30000+3000j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "unmappedGen1.power_out_B", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "power_out_B", "attack_value": "60000+12000j", "real_value": "30000+3000j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "unmappedGen1.power_out_C", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "power_out_C", "attack_value": "30000+5000j", "real_value": "30000+3000j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "unmappedGen2.power_out_A", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "power_out_A", "attack_value": "30000+5000j", "real_value": "25000+8333j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
30
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "unmappedGen2.power_out_B", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "power_out_B", "attack_value": "40000+8000j", "real_value": "25000+8333j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "unmappedGen2.power_out_C", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "power_out_C", "attack_value": "60000+12000j", "real_value": "25000+8333j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3Gen3.Qref", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "Qref", "attack_value": 2.38, "real_value": 2.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3Gen3.power_out_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "power_out_A", "attack_value": "30000+5000j", "real_value": "50000+16667j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
31
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3Gen3.power_out_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "power_out_B", "attack_value": "40000+8000j", "real_value": "50000+16667j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3Gen3.power_out_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "power_out_C", "attack_value": "30000+5000j", "real_value": "50000+16667j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "unmappedGen4.power_out_A", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "power_out_A", "attack_value": "30000+5000j", "real_value": "50000+16667j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "unmappedGen4.power_out_B", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "power_out_B", "attack_value": "40000+8000j", "real_value": "50000+16667j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
32
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "unmappedGen4.power_out_C", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "power_out_C", "attack_value": "30000+5000j", "real_value": "50000+16667j", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1trip_shad_inv1.Pref", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "Pref", "attack_value": 390014.906, "real_value": 450000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1trip_shad_inv1.Qref", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "Qref", "attack_value": 6670.42, "real_value": 0.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "unmappedtrip_shad_inv2.Pref", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "Pref", "attack_value": 160264.176, "real_value": 126000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "unmappedtrip_shad_inv2.Qref", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "Qref", "attack_value": 10325.924, "real_value": 0.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
33
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM2.mg2trip_shad_inv3.Pref", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "Pref", "attack_value": 107440.611, "real_value": 84000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM2.mg2trip_shad_inv3.Qref", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "Qref", "attack_value": 8296.65, "real_value": 0.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1trip_shad_inv4.Pref", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "Pref", "attack_value": 151838.751, "real_value": 210000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM1.mg1trip_shad_inv4.Qref", "scope": {"mg": "mg1", "mim": "MIM1", "apply": "both"}, "op": "set", "point": "Qref", "attack_value": 13115.112, "real_value": 0.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
34
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "unmappedtrip_shad_inv5.Pref", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "Pref", "attack_value": 405788.64, "real_value": 300000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "unmappedtrip_shad_inv5.Qref", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "Qref", "attack_value": 13060.786, "real_value": 0.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "unmappedtrip_shad_inv6.Pref", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "Pref", "attack_value": 86791.111, "real_value": 70000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "unmappedtrip_shad_inv6.Qref", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "Qref", "attack_value": 7267.395, "real_value": 0.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
35
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM4.substationmicrogrid_switch0.status", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3switch_76-86.status", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3switch_76-761.status", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationmicrogrid_switch1.status", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
36
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3microgrid_switch2.status", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationmicrogrid_switch3.status", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM2.mg2microgrid_switch4.status", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationswitch_13-152.status", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationswitch_61-611.status", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
37
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "unmappedswitch_1491-149.status", "scope": {"mg": "unmapped", "mim": null, "apply": "glm_only"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationswitch_250-251.status", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3switch_450-451.status", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM4.substationswitch_54-94.status", "scope": {"mg": "substation", "mim": "MIM4", "apply": "both"}, "op": "set", "point": "status", "attack_value": "CLOSED", "real_value": "OPEN", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM2.mg2switch_300-350.status", "scope": {"mg": "mg2", "mim": "MIM2", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
38
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3switch_95-195.status", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3switch_100-101.status", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "status", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3cap_83.switchA", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "switchA", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3cap_83.switchB", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "switchB", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3cap_83.switchC", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "switchC", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
39
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3cap_83.capacitor_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "capacitor_A", "attack_value": 171112.509, "real_value": 200000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3cap_83.capacitor_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "capacitor_B", "attack_value": 105796.03, "real_value": 200000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3cap_83.capacitor_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "capacitor_C", "attack_value": 105587.415, "real_value": 200000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3cap_88.switchA", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "switchA", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3cap_88.capacitor_A", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "capacitor_A", "attack_value": 38970.927, "real_value": 50000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
40
+ {"version": "1.1", "time": {"start_s": 0.0, "end_s": 60.0}, "mim": {"active": true, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]}, "plan": [{"name": "MIM3.mg3cap_90.switchB", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "switchB", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3cap_90.capacitor_B", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "capacitor_B", "attack_value": 37958.718, "real_value": 50000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3cap_92.switchC", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "switchC", "attack_value": "OPEN", "real_value": "CLOSED", "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}, {"name": "MIM3.mg3cap_92.capacitor_C", "scope": {"mg": "mg3", "mim": "MIM3", "apply": "both"}, "op": "set", "point": "capacitor_C", "attack_value": 59626.097, "real_value": 50000.0, "phase": null, "window": {"point_start_s": 1.0, "point_stop_s": 20.0}}], "compile_hints": {"scenario_id": "a"}}
scripts/train_attackplan.jsonl ADDED
@@ -0,0 +1,269 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+ """
3
+ Created on Sun Aug 17 19:47:52 2025
4
+
5
+ @author: adetu
6
+ """
7
+
8
+ # -*- coding: utf-8 -*-
9
+ """
10
+ make_train_jsonl.py — builds training JSONL for AttackPlan v1.1 (and a chat sample)
11
+
12
+ Outputs (saved two folders above your workspace, under scripts/):
13
+ - train_attackplan.jsonl # one AttackPlan v1.1 per line
14
+ - train_chat.jsonl # one chat-style example (system/user -> assistant JSON)
15
+ - train_preview.csv # quick preview of first ~30 items
16
+
17
+ Run (from EditGlm/ as CWD):
18
+ %run scripts/make_train_jsonl.py --n 400 --seed 7
19
+ """
20
+
21
+ import argparse, json, os, random, sys
22
+ from pathlib import Path
23
+ from typing import Dict, Any, List, Tuple
24
+
25
+ import pandas as pd
26
+
27
+
28
+ sys.path.insert(0, os.getcwd())
29
+
30
+ # Your libs
31
+ from libraries import BikdashGLM as BG
32
+ from libraries import IreNatJson as INJ
33
+
34
+ ATTACKPLAN_VERSION = "1.1"
35
+
36
+
37
+ # Filesystem helpers
38
+
39
+ def _folders_files(Remarks: dict):
40
+ res = BG.FoldersAndFiles(Remarks)
41
+ if isinstance(res, tuple):
42
+ if len(res) == 2:
43
+ Folders, Files = res
44
+ return Folders, Files, Remarks
45
+ elif len(res) == 3:
46
+ Folders, Files, Remarks = res
47
+ return Folders, Files, Remarks
48
+ raise RuntimeError("Unexpected return from BG.FoldersAndFiles(Remarks)")
49
+
50
+ def _read_initial_glm(Files: dict) -> str:
51
+ with open(Files["initialGlm"], "r", encoding="utf-8") as f:
52
+ return f.read()
53
+
54
+ def _load_ngj(Files: dict) -> dict:
55
+ """Load/parse your combined JSON into NGJ and expand mg lookup."""
56
+ json_text = Path(Files["combinedJson"]).read_text(encoding="utf-8")
57
+ ng = INJ.parse_ngjson(Files, json_text)
58
+ NGJ = INJ.getNGJ(ng)
59
+ BG.expand_mg_info(NGJ) # builds mg_device_lookup, etc.
60
+ return NGJ
61
+
62
+ def _build_topology(ELEMglm: dict, Topol: dict, NGJ: dict) -> dict:
63
+ BG.getTopol(ELEMglm, Topol)
64
+ Topol["mg_device_lookup"] = NGJ.get("mg_device_lookup", {})
65
+ Topol.update(INJ.microgrid_mapping(NGJ)) # adds mg_map, etc.
66
+ return Topol
67
+
68
+ def _scope_map_from_topol_and_glm(Topol: dict, ELEMglm: dict) -> Dict[str, Dict[str, str | None]]:
69
+ """device -> {'mg': 'mg1|mg2|mg3|substation|unmapped', 'mim': 'MIM1'..'MIM4'|None}"""
70
+ scope = {}
71
+ mg_map: Dict[str, Dict[str, str]] = Topol.get("mg_map", {}) or {}
72
+ objTypes = ['switch', 'load', 'inverter_dyn', 'diesel_dg', 'capacitor', 'regulator']
73
+ try:
74
+ names, _ = BG.extractNamesTypes(ELEMglm, objTypes)
75
+ except Exception:
76
+ names = []
77
+ for t in objTypes:
78
+ for blk in ELEMglm.get(t, []):
79
+ try:
80
+ nm = BG.extractNameOfGlmObject(blk)
81
+ if nm: names.append(nm)
82
+ except Exception:
83
+ pass
84
+ names = list(dict.fromkeys(names))
85
+ for dev in names:
86
+ ent = mg_map.get(dev)
87
+ if isinstance(ent, dict) and ent.get("mg") and ent.get("mim"):
88
+ scope[dev] = {"mg": ent["mg"], "mim": ent["mim"]}
89
+ else:
90
+ scope[dev] = {"mg": "unmapped", "mim": None}
91
+ return scope
92
+
93
+ def _name_from_scope(dev: str, prop: str, scope: Dict[str, Dict[str, str | None]]) -> str:
94
+ """Build 'MIMx.mgDevice.property' if mapped; else 'mg?Device.property' (schema allows missing MIM)."""
95
+ ent = scope.get(dev, {"mg": "unmapped", "mim": None})
96
+ mg = ent.get("mg") or "unmapped"
97
+ mim = ent.get("mim")
98
+ base = f"{mg}{dev}.{prop}"
99
+ return f"{mim}.{base}" if mim else base
100
+
101
+ def _window_default() -> Dict[str, float]:
102
+ return {"point_start_s": 1.0, "point_stop_s": 20.0}
103
+
104
+ def _attackplan_skeleton() -> Dict[str, Any]:
105
+ return {
106
+ "version": ATTACKPLAN_VERSION,
107
+ "time": {"start_s": 0.0, "end_s": 60.0},
108
+ "mim": {"active": True, "selected": ["MIM1", "MIM2", "MIM3", "MIM4"]},
109
+ "plan": [],
110
+ "compile_hints": {"scenario_id": "a"}
111
+ }
112
+
113
+ # Value transforms (local)
114
+
115
+ def _flip_status(val: Any) -> str:
116
+ s = str(val).strip().lower()
117
+ if s in {"open", "0", "false", "off"}: return "CLOSED"
118
+ if s in {"closed", "1", "true", "on"}: return "OPEN"
119
+ return "OPEN" if "open" not in s else "CLOSED"
120
+
121
+ def _to_float(val: Any) -> float | None:
122
+ try:
123
+ return float(val)
124
+ except Exception:
125
+ return None
126
+
127
+ def _rand_scale(num: float, rng: random.Random) -> float:
128
+ # scale in [0.5, 1.5] (adjust if you prefer)
129
+ return num * rng.uniform(0.5, 1.5)
130
+
131
+ # Main item builder (uses INJ.extract_baseline)
132
+
133
+ def _items_from_baseline(ELEMglm: dict,
134
+ scope_lookup: Dict[str, Dict[str, str | None]],
135
+ n: int | None = None,
136
+ seed: int = 7) -> List[Dict[str, Any]]:
137
+ """
138
+ Use IreNatJson.extract_baseline(ELEMglm) to get {(device, prop): value} and dev types,
139
+ then create AttackPlan v1.1 plan items by flipping status / tweaking numeric values.
140
+ """
141
+ rng = random.Random(seed)
142
+ baseline, dev_type = INJ.extract_baseline(ELEMglm) # returns dict, dict
143
+
144
+ # Optional downsample
145
+ pairs = list(baseline.items())
146
+ if n is not None and n > 0 and len(pairs) > n:
147
+ rng.shuffle(pairs)
148
+ pairs = pairs[:n]
149
+
150
+ items: List[Dict[str, Any]] = []
151
+ for (device_name, property_name), original_val in pairs:
152
+ # Decide new value
153
+ if property_name.lower() in {"status", "switcha", "switchb", "switchc"}:
154
+ new_val = _flip_status(original_val)
155
+ elif str(property_name).startswith("power_out_"): # generator complex literals
156
+ new_val = rng.choice(["60000+12000j", "40000+8000j", "30000+5000j"])
157
+ else:
158
+ num = _to_float(original_val)
159
+ if num is None:
160
+ # fallback if unparsable numeric — skip this property
161
+ continue
162
+ # Prefer not to keep 0 for inverter Pref/Qref → give a small nonzero base
163
+ if (dev_type.get(device_name, "").startswith("inverter")
164
+ and property_name in ("Pref", "Qref") and num == 0):
165
+ num = 10000.0 # adjust if you have a domain-specific default
166
+ new_val = round(_rand_scale(num, rng), 3)
167
+
168
+ # Build plan item
169
+ name = _name_from_scope(device_name, property_name, scope_lookup)
170
+ ent = scope_lookup.get(device_name, {"mg": "unmapped", "mim": None})
171
+ scope = {"mg": ent.get("mg"), "mim": ent.get("mim"), "apply": "both" if ent.get("mim") else "glm_only"}
172
+ item = {
173
+ "name": name,
174
+ "scope": scope,
175
+ "op": "set", # normalized; compiler can map to open/close/trip later
176
+ "point": property_name,
177
+ "attack_value": new_val,
178
+ "real_value": original_val,
179
+ "phase": None,
180
+ "window": _window_default(),
181
+ }
182
+ items.append(item)
183
+ return items
184
+
185
+ # Output packers
186
+
187
+ def _chat_pair_from_items(items: List[Dict[str, Any]]) -> Dict[str, Any]:
188
+ plan = _attackplan_skeleton()
189
+ plan["plan"] = items
190
+ return {
191
+ "messages": [
192
+ {"role": "system", "content": "You output ONLY JSON, no explanation."},
193
+ {"role": "user", "content": "Generate an AttackPlan JSON v1.1 for the following actions. Respect microgrid scope; if a device is unmapped, mark it glm_only. Return ONLY the JSON."},
194
+ {"role": "assistant", "content": json.dumps(plan, ensure_ascii=False)}
195
+ ]
196
+ }
197
+
198
+ def _attackplan_lines_from_items(items: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
199
+ lines = []
200
+ pack = 5 # 3–6 items per plan; adjust as you like
201
+ for i in range(0, len(items), pack):
202
+ plan = _attackplan_skeleton()
203
+ plan["plan"] = items[i:i+pack]
204
+ if plan["plan"]:
205
+ lines.append(plan)
206
+ return lines
207
+
208
+ # Main
209
+
210
+ def main():
211
+ ap = argparse.ArgumentParser()
212
+ ap.add_argument("--n", type=int, default=400, help="Max number of (device,property) pairs to sample.")
213
+ ap.add_argument("--seed", type=int, default=7)
214
+ args = ap.parse_args()
215
+
216
+ Remarks = BG.initRemarks()
217
+ Folders, Files, Remarks = _folders_files(Remarks)
218
+
219
+ # Output root: two folders above workspace, into scripts/
220
+ ws = Path(Folders["workspace"]).resolve()
221
+ out_root = ws.parents[1] / "scripts"
222
+ out_root.mkdir(parents=True, exist_ok=True)
223
+
224
+ # Load inputs
225
+ initialGlm = _read_initial_glm(Files)
226
+ ELEMglm, Topol = BG.getELEMs(Files, initialGlm)
227
+ NGJ = _load_ngj(Files)
228
+ Topol = _build_topology(ELEMglm, Topol, NGJ)
229
+ scope_lookup = _scope_map_from_topol_and_glm(Topol, ELEMglm)
230
+
231
+ # Build items using IreNatJson.extract_baseline
232
+ items = _items_from_baseline(ELEMglm, scope_lookup, n=args.n, seed=args.seed)
233
+
234
+ # Pack outputs
235
+ chat_line = _chat_pair_from_items(items[:min(25, len(items))])
236
+ plan_lines = _attackplan_lines_from_items(items)
237
+
238
+ # Write
239
+ attackplan_path = out_root / "train_attackplan.jsonl"
240
+ chat_path = out_root / "train_chat.jsonl"
241
+ preview_path = out_root / "train_preview.csv"
242
+
243
+ with attackplan_path.open("w", encoding="utf-8") as f:
244
+ for plan in plan_lines:
245
+ f.write(json.dumps(plan, ensure_ascii=False) + "\n")
246
+
247
+ with chat_path.open("w", encoding="utf-8") as f:
248
+ f.write(json.dumps(chat_line, ensure_ascii=False) + "\n")
249
+
250
+ preview = [{
251
+ "name": it["name"],
252
+ "mg": (it.get("scope") or {}).get("mg"),
253
+ "mim": (it.get("scope") or {}).get("mim"),
254
+ "apply": (it.get("scope") or {}).get("apply"),
255
+ "op": it["op"],
256
+ "point": it["point"],
257
+ "attack_value": it["attack_value"],
258
+ "real_value": it["real_value"],
259
+ "start": it["window"]["point_start_s"],
260
+ "stop": it["window"]["point_stop_s"],
261
+ } for it in items[:30]]
262
+ pd.DataFrame(preview).to_csv(preview_path, index=False)
263
+
264
+ print(f"[ok] wrote {attackplan_path}")
265
+ print(f"[ok] wrote {chat_path}")
266
+ print(f"[ok] wrote {preview_path}")
267
+
268
+ if __name__ == "__main__":
269
+ main()
scripts/train_qlora.py ADDED
@@ -0,0 +1,226 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # train_qlora.py
2
+ # QLoRA fine tuning for chat JSONL built from attack plans
3
+ # Works well with deepseek-ai/deepseek-coder-6.7b-instruct on Colab Pro GPUs
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ from pathlib import Path
9
+ from typing import Dict, List, Union
10
+
11
+ import torch
12
+ from datasets import load_dataset
13
+ from transformers import (
14
+ AutoTokenizer,
15
+ AutoModelForCausalLM,
16
+ BitsAndBytesConfig,
17
+ Trainer,
18
+ TrainingArguments,
19
+ )
20
+ from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
21
+
22
+
23
+ def parse_args():
24
+ ap = argparse.ArgumentParser()
25
+ ap.add_argument("--base", type=str, required=True, help="Base model id or path")
26
+ ap.add_argument("--data", type=str, required=True, help="JSONL with chat messages")
27
+ ap.add_argument("--out", type=str, required=True, help="Output dir for adapter")
28
+ ap.add_argument("--epochs", type=int, default=2)
29
+ ap.add_argument("--bsz", type=int, default=8)
30
+ ap.add_argument("--grad_accum", type=int, default=1)
31
+ ap.add_argument("--cutoff_len", type=int, default=2048)
32
+ ap.add_argument("--lr", type=float, default=2e-4)
33
+ ap.add_argument("--lora_r", type=int, default=16)
34
+ ap.add_argument("--lora_alpha", type=int, default=32)
35
+ ap.add_argument("--lora_dropout", type=float, default=0.05)
36
+ ap.add_argument("--debug", action="store_true")
37
+ return ap.parse_args()
38
+
39
+
40
+ def device_supports_bf16() -> bool:
41
+ if not torch.cuda.is_available():
42
+ return False
43
+ major, _ = torch.cuda.get_device_capability(0)
44
+ return major >= 8 # Ampere or newer
45
+
46
+
47
+ def build_tokenizer(base_id: str):
48
+ tok = AutoTokenizer.from_pretrained(base_id, use_fast=True)
49
+ if tok.pad_token is None:
50
+ tok.pad_token = tok.eos_token
51
+ tok.padding_side = "right"
52
+ return tok
53
+
54
+
55
+ def _to_ids(x: Union[torch.Tensor, List[int], Dict[str, List[int]]]) -> List[int]:
56
+ if isinstance(x, torch.Tensor):
57
+ return x.detach().cpu().tolist()[0] if x.ndim == 2 else x.detach().cpu().tolist()
58
+ if isinstance(x, dict) and "input_ids" in x:
59
+ return x["input_ids"]
60
+ if isinstance(x, (list, tuple)):
61
+ return list(x)
62
+ raise TypeError(f"Unsupported chat template return type: {type(x)}")
63
+
64
+
65
+ def chat_to_ids(tokenizer: AutoTokenizer, messages: List[Dict], max_len: int):
66
+ # Prefer native chat template. In recent Transformers this returns a tensor
67
+ # when return_tensors is set, or a list of token ids when tokenize is True.
68
+ if hasattr(tokenizer, "apply_chat_template") and tokenizer.chat_template:
69
+ out = tokenizer.apply_chat_template(
70
+ messages,
71
+ tokenize=True,
72
+ add_generation_prompt=False,
73
+ return_tensors="pt",
74
+ max_length=max_len,
75
+ truncation=True,
76
+ )
77
+ ids = _to_ids(out)
78
+ attn = [1] * len(ids)
79
+ return {"input_ids": ids, "attention_mask": attn}
80
+
81
+ # Fallback when no chat template is available
82
+ lines = []
83
+ for m in messages:
84
+ role = m.get("role", "user")
85
+ content = m.get("content", "")
86
+ lines.append(f"{role}:\n{content}\n")
87
+ text = "\n".join(lines)
88
+ enc = tokenizer(text, max_length=max_len, truncation=True)
89
+ return {"input_ids": enc["input_ids"], "attention_mask": enc["attention_mask"]}
90
+
91
+
92
+ def collate_pad(tokenizer: AutoTokenizer):
93
+ pad_id = tokenizer.pad_token_id
94
+
95
+ def _fn(batch: List[Dict[str, List[int]]]):
96
+ max_len = max(len(x["input_ids"]) for x in batch)
97
+ input_ids, attn, labels = [], [], []
98
+ for x in batch:
99
+ ids = x["input_ids"]
100
+ am = x["attention_mask"]
101
+ pad_n = max_len - len(ids)
102
+ input_ids.append(ids + [pad_id] * pad_n)
103
+ attn.append(am + [0] * pad_n)
104
+ labels.append(ids + [-100] * pad_n)
105
+ return {
106
+ "input_ids": torch.tensor(input_ids, dtype=torch.long),
107
+ "attention_mask": torch.tensor(attn, dtype=torch.long),
108
+ "labels": torch.tensor(labels, dtype=torch.long),
109
+ }
110
+
111
+ return _fn
112
+
113
+
114
+ def guess_lora_targets(model: torch.nn.Module) -> List[str]:
115
+ prefs = [
116
+ "q_proj",
117
+ "k_proj",
118
+ "v_proj",
119
+ "o_proj",
120
+ "gate_proj",
121
+ "up_proj",
122
+ "down_proj",
123
+ "wi",
124
+ "wo",
125
+ "w1",
126
+ "w2",
127
+ "w3",
128
+ "out_proj",
129
+ ]
130
+ found = set()
131
+ for name, _ in model.named_modules():
132
+ for p in prefs:
133
+ if p in name:
134
+ found.add(p)
135
+ return sorted(found) if found else ["Linear"]
136
+
137
+
138
+ def main():
139
+ args = parse_args()
140
+ base_id = args.base
141
+ data_path = Path(args.data)
142
+ out_dir = Path(args.out)
143
+ out_dir.mkdir(parents=True, exist_ok=True)
144
+
145
+ tokenizer = build_tokenizer(base_id)
146
+
147
+ ds = load_dataset("json", data_files=str(data_path), split="train")
148
+
149
+ def map_row(ex):
150
+ return chat_to_ids(tokenizer, ex["messages"], args.cutoff_len)
151
+
152
+ # Remove original columns after mapping so only model fields remain
153
+ ds = ds.map(map_row, remove_columns=ds.column_names)
154
+
155
+ collate = collate_pad(tokenizer)
156
+
157
+ quant = BitsAndBytesConfig(
158
+ load_in_4bit=True,
159
+ bnb_4bit_quant_type="nf4",
160
+ bnb_4bit_use_double_quant=True,
161
+ )
162
+
163
+ use_bf16 = device_supports_bf16()
164
+ torch_dtype = torch.bfloat16 if use_bf16 else torch.float16
165
+ torch.backends.cuda.matmul.allow_tf32 = True
166
+
167
+ model = AutoModelForCausalLM.from_pretrained(
168
+ base_id,
169
+ device_map="auto",
170
+ quantization_config=quant,
171
+ torch_dtype=torch_dtype,
172
+ )
173
+
174
+ model = prepare_model_for_kbit_training(model)
175
+ lconf = LoraConfig(
176
+ r=args.lora_r,
177
+ lora_alpha=args.lora_alpha,
178
+ lora_dropout=args.lora_dropout,
179
+ bias="none",
180
+ task_type="CAUSAL_LM",
181
+ target_modules=guess_lora_targets(model),
182
+ )
183
+ model = get_peft_model(model, lconf)
184
+
185
+ train_args = TrainingArguments(
186
+ output_dir=str(out_dir),
187
+ num_train_epochs=args.epochs,
188
+ per_device_train_batch_size=args.bsz,
189
+ gradient_accumulation_steps=args.grad_accum,
190
+ learning_rate=args.lr,
191
+ lr_scheduler_type="cosine",
192
+ warmup_ratio=0.03,
193
+ logging_steps=5,
194
+ save_steps=100,
195
+ bf16=use_bf16,
196
+ fp16=not use_bf16,
197
+ optim="paged_adamw_8bit",
198
+ remove_unused_columns=False,
199
+ dataloader_num_workers=2,
200
+ report_to=[],
201
+ )
202
+
203
+ tr = Trainer(
204
+ model=model,
205
+ args=train_args,
206
+ train_dataset=ds,
207
+ data_collator=collate,
208
+ tokenizer=tokenizer,
209
+ )
210
+
211
+ if args.debug:
212
+ batch = next(iter(tr.get_train_dataloader()))
213
+ print("[debug] batch keys:", list(batch.keys()))
214
+ for k, v in batch.items():
215
+ if isinstance(v, torch.Tensor):
216
+ print(f"[debug] {k}: shape={tuple(v.shape)} dtype={v.dtype}")
217
+
218
+ tr.train()
219
+
220
+ model.save_pretrained(str(out_dir))
221
+ tokenizer.save_pretrained(str(out_dir))
222
+ print("[ok] saved adapter to", out_dir.resolve())
223
+
224
+
225
+ if __name__ == "__main__":
226
+ main()
scripts/validate_attackplan_jsonl.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Sun Aug 17 20:36:17 2025
4
+
5
+ @author: adetu
6
+ """
7
+
8
+ import json, sys
9
+ from json import JSONDecodeError
10
+ from pathlib import Path
11
+ from jsonschema import Draft202012Validator as V
12
+
13
+ def load_schema():
14
+ candidates = [
15
+ Path("schemas/attack_plan.schema.json"),
16
+ Path("../schemas/attack_plan.schema.json"),
17
+ ]
18
+ for p in candidates:
19
+ if p.exists():
20
+ raw = p.read_text(encoding="utf-8-sig") # handle BOM
21
+ # strip accidental markdown fences
22
+ lines = [ln for ln in raw.splitlines() if not ln.strip().startswith("```")]
23
+ raw = "\n".join(lines).strip()
24
+ if not raw:
25
+ raise RuntimeError(f"Schema file is empty: {p}")
26
+ try:
27
+ schema = json.loads(raw)
28
+ print(f"[schema] loaded {p.resolve()}")
29
+ return schema
30
+ except JSONDecodeError as e:
31
+ ctx = raw.splitlines()[max(e.lineno-2,0):e.lineno+1]
32
+ print(f"[schema] JSON error in {p}: {e.msg} at line {e.lineno}, col {e.colno}")
33
+ print("Context:\n" + "\n".join(ctx))
34
+ raise
35
+ raise FileNotFoundError("Could not find schema at schemas/attack_plan.schema.json")
36
+
37
+ def main():
38
+ schema = load_schema()
39
+ validator = V(schema)
40
+
41
+ src = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("scripts/train_attackplan.jsonl")
42
+ raw = src.read_text(encoding="utf-8")
43
+ valid = invalid = 0
44
+ for i, line in enumerate(raw.splitlines(), 1):
45
+ if not line.strip(): # skip blanks
46
+ continue
47
+ try:
48
+ obj = json.loads(line)
49
+ except JSONDecodeError as e:
50
+ print(f"[line {i}] not JSON: {e.msg} at {e.lineno}:{e.colno}")
51
+ print(" snippet:", line[:200])
52
+ invalid += 1
53
+ continue
54
+
55
+ errs = sorted(validator.iter_errors(obj), key=lambda e: (list(e.path), e.message))
56
+ if errs:
57
+ invalid += 1
58
+ print(f"[line {i}] INVALID:")
59
+ for e in errs[:8]:
60
+ print(" -", e.message, "at", list(e.path))
61
+ else:
62
+ valid += 1
63
+ print(f"[done] {valid} valid, {invalid} invalid")
64
+
65
+ if __name__ == "__main__":
66
+ main()
scripts/write_glm_snippets.py ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ write_glm_snippets.py
4
+ Write a few canonical GLM snippets under kb/snippets/glm for reference.
5
+ These are small, human-readable examples — not full runnable feeders.
6
+
7
+ Usage:
8
+ %run scripts/write_glm_snippets.py
9
+ """
10
+ from pathlib import Path
11
+
12
+ SNIPS = {
13
+ "open_switch_status.glm": r"""// Minimal switch example (OPEN)
14
+ object switch {
15
+ name microgrid_switch_YYY;
16
+ status OPEN; // CLOSE to reconnect
17
+ }
18
+ """,
19
+ "close_switch_status.glm": r"""// Minimal switch example (CLOSED)
20
+ object switch {
21
+ name microgrid_switch_YYY;
22
+ status CLOSED;
23
+ }
24
+ """,
25
+ "set_inverter_Pref_Qref.glm": r"""// Inverter real/reactive setpoints (values indicative)
26
+ object inverter_dyn {
27
+ name inverter_XXX;
28
+ Pref 10000; // [W]
29
+ Qref 0; // [var]
30
+ // Pmax 50000; // [W] (if present in your models)
31
+ // Qmax 30000; // [var]
32
+ // Vset 4800; // [mV] or [V] depending on your modeling (verify in your pipeline)
33
+ }
34
+ """,
35
+ "set_load_constant_power_A.glm": r"""// Load constant power (phase A)
36
+ object load {
37
+ name load_41;
38
+ constant_power_A 25000; // [VA], can be complex like 60000+12000j
39
+ }
40
+ """,
41
+ "set_generator_power_out.glm": r"""// Diesel generator phase power outputs
42
+ object diesel_dg {
43
+ name gen_01;
44
+ power_out_A 60000+12000j; // [VA], complex
45
+ // power_out_B 40000+8000j;
46
+ // power_out_C 30000+5000j;
47
+ }
48
+ """,
49
+ "set_capacitor_phase.glm": r"""// Capacitor phase state/setting (model-specific)
50
+ object capacitor {
51
+ name cap_01;
52
+ // Some models use per-phase flags or discrete steps; confirm with your code.
53
+ // Examples (choose one representation your pipeline expects):
54
+ // capacitor_A CLOSED;
55
+ // capacitor_B OPEN;
56
+ // capacitor_C CLOSED;
57
+ }
58
+ """,
59
+ "set_regulator_tap.glm": r"""// Regulator tap steps (example)
60
+ object regulator {
61
+ name reg_01;
62
+ tap_A 3; // integer step
63
+ tap_B 2;
64
+ tap_C 4;
65
+ }
66
+ """,
67
+ "README.md": r"""These snippets are tiny GLM fragments used as **reference** for RAG.
68
+ They are not full feeders. Use them to:
69
+ - ground the LLM on property spellings and typical values,
70
+ - show OPEN/CLOSED vs numeric edits,
71
+ - remind how inverter/load/generator/regulator properties look in GLM.
72
+
73
+ **REALITY FILTER**:
74
+ - Where semantics are unclear, this folder includes comments marked [Inference] or asks you to verify in your pipeline.
75
+ - Prefer your actual GLM corpus (via RAG) for precise formatting when available.
76
+ """
77
+ }
78
+
79
+ def main():
80
+ outdir = Path("kb/snippets/glm")
81
+ outdir.mkdir(parents=True, exist_ok=True)
82
+ for fname, text in SNIPS.items():
83
+ (outdir / fname).write_text(text.strip() + "\n", encoding="utf-8")
84
+ print("[ok] wrote", len(SNIPS), "files to", outdir.resolve())
85
+
86
+ if __name__ == "__main__":
87
+ main()