Upload folder using huggingface_hub
Browse files- scripts/augment_ops_coverage.py +103 -0
- scripts/build_rag_index.py +50 -0
- scripts/filter_attackplan_jsonl.py +192 -0
- scripts/guardrails.py +224 -0
- scripts/make_chat_from_plans.py +46 -0
- scripts/make_property_glossary.py +95 -0
- scripts/run_hybrid_infer.py +340 -0
- scripts/seed_kb_examples.py +256 -0
- scripts/split_attackplan_jsonl.py +55 -0
- scripts/train_attackplan.aug.jsonl +0 -0
- scripts/train_attackplan.filtered.jsonl +40 -0
- scripts/train_attackplan.jsonl +269 -0
- scripts/train_qlora.py +226 -0
- scripts/validate_attackplan_jsonl.py +66 -0
- scripts/write_glm_snippets.py +87 -0
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()
|