forked from jursber/lmp-teaching
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsced_and_lmp_educational_system.html
More file actions
2424 lines (2220 loc) · 152 KB
/
Copy pathsced_and_lmp_educational_system.html
File metadata and controls
2424 lines (2220 loc) · 152 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>SCED / LMP 教学系统 — 5 节点</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css">
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
<style>
:root{
--bg:#f5f7fa; --card:#ffffff; --card-border:#d1d5db;
--card-shadow:0 4px 20px -8px rgba(0,0,0,0.05);
--divider:#c9ced4; --divider2:#d1d5db;
--ink:#1e293b; --body:#334155; --muted:#64748b; --dim:#94a3b8;
--primary:#2563eb; --primary-h:#1d4ed8; --primary-light:#eff6ff;
--success:#10b981; --success-light:#f0fdf4;
--warning:#f59e0b; --warning-light:#fef3c7;
--error:#ef4444; --error-light:#fef2f2;
--font:"Inter",-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"PingFang SC","Microsoft YaHei",sans-serif;
--mono:"JetBrains Mono","SF Mono",Consolas,monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{height:100%;overflow:hidden}
body{background:var(--bg);color:var(--body);font-family:var(--font);font-size:14px;line-height:1.5;-webkit-font-smoothing:antialiased}
button,input{font-family:inherit;font-size:inherit}
.app-shell{height:100vh;display:flex;flex-direction:column}
::-webkit-scrollbar{width:4px;height:4px}
::-webkit-scrollbar-track{background:transparent}
::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:2px}
/* Topbar */
.topbar{
display:flex;align-items:center;justify-content:space-between;
gap:16px;padding:0 24px;height:56px;flex-shrink:0;
background:var(--card);border-bottom:1px solid var(--card-border);
}
.brand{display:flex;align-items:center;gap:12px}
.brand-mark{
width:32px;height:32px;border-radius:8px;display:grid;place-items:center;
background:var(--primary-light);color:var(--primary);flex-shrink:0;
}
.brand-mark svg{width:16px;height:16px}
.brand h1{font-size:18px;font-weight:700;color:var(--ink);letter-spacing:-.01em}
.brand p{font-size:12px;color:var(--dim);margin-top:1px}
.live-badge{
display:inline-flex;align-items:center;gap:5px;
padding:3px 10px;border-radius:20px;
background:var(--success-light);color:var(--success);
font-size:11px;font-weight:600;
}
.live-dot{width:6px;height:6px;border-radius:50%;background:var(--success);animation:livePulse 2s infinite}
@keyframes livePulse{0%,100%{opacity:1}50%{opacity:.3}}
/* Workspace */
.workspace{
flex:1;min-height:0;display:grid;
grid-template-columns:340px 1fr 400px;
gap:12px;padding:12px;
}
.panel{
min-height:0;display:flex;flex-direction:column;
background:var(--card);border-radius:16px;
border:1px solid var(--card-border);
box-shadow:var(--card-shadow);overflow:hidden;
}
.panel-hd{
display:flex;align-items:center;justify-content:space-between;
padding:0 12px;height:36px;flex-shrink:0;
border-bottom:1px solid var(--divider);
}
.panel-hd h2{
font-size:13px;font-weight:600;color:var(--muted);
display:flex;align-items:center;gap:6px;
letter-spacing:.04em;text-transform:uppercase;
}
.panel-hd svg{width:13px;height:13px;color:var(--muted)}
.panel-bd{flex:1;min-height:0;overflow:auto;padding:12px}
/* Left Panel */
.sec-label{
color:var(--muted);font-size:11px;letter-spacing:.04em;
text-transform:uppercase;margin:10px 0 5px;font-weight:600;
display:flex;align-items:center;gap:6px;
}
.sec-label::before{content:"";width:3px;height:14px;background:var(--primary);border-radius:2px}
.sec-label:first-child{margin-top:0}
.gen-card,.line-card{
background:var(--bg);border-radius:8px;padding:8px 10px;margin-bottom:6px;
}
.gen-hd{display:flex;justify-content:space-between;align-items:center;margin-bottom:5px}
.gen-name{font-size:13px;font-weight:600;color:var(--ink)}
.gen-desc{font-size:11px;padding:2px 8px;border-radius:6px;font-weight:500;margin-left:8px}
.gen-desc-low{background:#dbeafe;color:var(--primary)}
.gen-desc-mid{background:var(--warning-light);color:#b45309}
.gen-desc-high{background:#f3f4f6;color:var(--muted)}
.gen-summary{font-size:11px;color:var(--muted);flex:1;text-align:right;margin-right:8px}
.gen-body{margin-top:8px;display:flex;flex-direction:column;gap:6px}
.gen-json{
width:100%;border:1px solid var(--card-border);background:var(--card);
color:var(--ink);border-radius:6px;padding:6px 8px;outline:none;
font-size:11px;font-family:var(--mono);resize:vertical;
}
.gen-json:focus{border-color:var(--primary)}
.gen-row{display:flex;gap:6px}
.gen-btn{
flex:1;border:none;border-radius:6px;padding:5px 0;cursor:pointer;
font-size:11px;font-weight:500;transition:all .15s;
}
.gen-btn-default{background:var(--bg);color:var(--muted)}
.gen-btn-default:hover{background:var(--divider2);color:var(--ink)}
.gen-btn-apply{background:var(--primary);color:#fff}
.gen-btn-apply:hover{background:var(--primary-h)}
.gen-toggle{cursor:pointer;font-size:11px;color:var(--dim);transition:transform .15s;display:inline-block}
.gen-toggle.open{transform:rotate(90deg)}
.fgrid{display:grid;grid-template-columns:1fr 1fr;gap:6px}
.fld{display:flex;flex-direction:column;gap:2px}
.fld label{color:var(--muted);font-size:12px;font-weight:500}
.fld input[type="number"]{
width:100%;border:1px solid var(--card-border);background:var(--card);
color:var(--ink);border-radius:6px;padding:4px 6px;outline:none;
font-size:12px;height:28px;font-family:var(--mono);
transition:all .2s;
}
.fld input:focus{border-color:var(--primary);box-shadow:0 0 0 3px rgba(37,99,235,0.1)}
.fld input[type="range"]{padding:0;border:0;background:transparent;height:22px;-webkit-appearance:none;appearance:none;width:100%}
.fld input[type="range"]::-webkit-slider-runnable-track{height:6px;border-radius:3px;background:linear-gradient(to right,var(--primary) 0%,var(--primary) var(--pct,50%),#94a3b8 var(--pct,50%),#94a3b8 100%)}
.fld input[type="range"]::-webkit-slider-thumb{
-webkit-appearance:none;width:18px;height:18px;border-radius:50%;
background:var(--card);border:2.5px solid var(--primary);cursor:pointer;margin-top:-6px;
box-shadow:0 1px 4px rgba(0,0,0,0.15);transition:all .15s;
}
.fld input[type="range"]::-webkit-slider-thumb:hover{transform:scale(1.15)}
.lgrid{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
.btn-run{
width:100%;border:none;background:var(--ink);color:#fff;
padding:10px;border-radius:8px;cursor:pointer;
font-weight:600;font-size:13px;margin-top:8px;
transition:all .2s;
}
.btn-run:hover{background:#334155;box-shadow:0 4px 12px rgba(15,23,42,0.15)}
/* Center Stack */
.center-stack{display:flex;flex-direction:column;gap:12px;min-height:0}
.topology{position:relative;padding:8px}
.topology svg{width:100%;height:100%;display:block}
.error-overlay{
position:absolute;inset:16px;display:grid;place-items:center;gap:8px;text-align:center;
background:rgba(255,255,255,0.95);border:1px solid var(--card-border);
border-radius:12px;padding:20px;
}
.hidden{display:none !important}
/* Result Panel */
.result-bd{display:flex;flex-direction:column;gap:12px;overflow:hidden}
.kpi-row{display:grid;grid-template-columns:repeat(4,1fr);gap:0}
.kpi{padding:12px 16px}
.kpi:not(:last-child){border-right:1px solid var(--divider)}
.kpi-label{color:var(--dim);font-size:11px;text-transform:uppercase;letter-spacing:.05em;font-weight:500}
.kpi-val{font-size:18px;font-weight:700;margin-top:4px;color:var(--ink);font-family:var(--mono)}
.data-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}
.data-section-title{color:var(--muted);font-size:11px;font-weight:600;margin-bottom:6px;letter-spacing:.03em;text-transform:uppercase}
.lmp-row{
display:flex;justify-content:space-between;align-items:center;
padding:5px 0;border-bottom:1px solid var(--divider2);font-size:13px;
}
.lmp-row:last-child{border-bottom:none}
.lmp-tag{font-weight:600;font-size:13px;color:var(--ink)}
.lmp-info{display:flex;flex-direction:column;align-items:flex-end;gap:0}
.lmp-info strong{font-size:14px;font-family:var(--mono);color:var(--ink)}
.lmp-comp{font-size:10px;color:var(--dim);font-family:var(--mono)}
.gen-tbl{width:100%;border-collapse:collapse;font-size:13px}
.gen-tbl th{color:var(--dim);font-size:10px;text-transform:uppercase;text-align:left;padding:4px 0;font-weight:600;letter-spacing:.04em}
.gen-tbl td{padding:4px 0;border-bottom:1px solid var(--divider2);font-family:var(--mono)}
.gen-tbl td:last-child,.gen-tbl th:last-child{text-align:right}
/* Tabs */
.tabs{display:flex;gap:0;border-bottom:1px solid var(--divider2);flex-shrink:0}
.tab-btn{
border:none;background:transparent;color:var(--muted);padding:7px 14px;
cursor:pointer;font-size:12px;font-weight:500;border-bottom:2px solid transparent;
transition:all .15s;white-space:nowrap;
}
.tab-btn:hover{color:var(--body)}
.tab-btn.active{color:var(--primary);border-bottom-color:var(--primary);font-weight:600}
.tab-content{display:none;overflow:auto}
.tab-content.active{display:block}
.derivation{margin-bottom:10px}
.derivation summary{
cursor:pointer;font-size:13px;font-weight:600;color:var(--ink);
padding:5px 0;list-style:none;display:flex;align-items:center;gap:6px;
}
.derivation summary::before{content:"▸";font-size:11px;color:var(--muted);transition:transform .15s}
.derivation[open] summary::before{transform:rotate(90deg)}
.derivation .step{margin:6px 0 6px 14px;padding:8px 12px;background:var(--bg);border-radius:8px;border-left:3px solid var(--primary)}
.derivation .step-desc{font-size:13px;color:var(--body);line-height:1.7}
.formula-box{margin:6px 0;padding:8px 12px;background:var(--card);border:1px solid var(--divider2);border-radius:6px;overflow-x:auto}
.verify-box{margin-top:10px;padding:10px 12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:8px;font-size:13px;color:#166534;line-height:1.7}
/* Fullscreen */
.center-stack.fullscreen .panel:first-child{display:none}
.center-stack.fullscreen .panel:last-child{flex:1 !important}
.btn-fullscreen{
border:none;cursor:pointer;font-size:11px;font-weight:600;
padding:4px 10px;border-radius:6px;transition:all .2s;
background:linear-gradient(135deg,#2563eb,#0ea5e9);color:#fff;
}
.btn-fullscreen:hover{transform:scale(1.05)}
/* Animations */
@keyframes arrowPulse{0%,100%{opacity:1}50%{opacity:.3}}
.flow-line{animation:flow 3s linear infinite}
@keyframes flow{from{stroke-dashoffset:0}to{stroke-dashoffset:40}}
/* ===== AI Panel ===== */
.ai-panel{position:relative}
#ai_body{flex:1;min-height:0;display:flex;flex-direction:column}
#ai_messages{flex:1;min-height:0;overflow-y:auto;overflow-x:hidden;padding:12px}
.ai-msg{margin-bottom:8px;max-width:92%;font-size:13px;line-height:1.7;word-break:break-word;overflow-wrap:anywhere}
#ai_messages .katex-display{max-width:100%;overflow-x:auto;overflow-y:hidden;text-align:initial;padding:2px 1px 6px;margin:.5em 0}
#ai_messages .katex-display::-webkit-scrollbar{height:6px}
#ai_messages .katex-display::-webkit-scrollbar-thumb{background:var(--divider);border-radius:3px}
.ai-msg.user{margin-left:auto;padding:8px 12px;border-radius:8px;background:var(--primary-light);color:var(--ink);font-weight:500}
.ai-msg.assistant{margin-right:auto;padding:10px 12px;border-radius:8px;background:var(--bg);color:var(--body)}
.ai-msg.error{margin-right:auto;padding:8px 12px;border-radius:8px;background:var(--error-light);color:var(--error)}
.ai-loading{display:flex;gap:5px;padding:10px 0}
.ai-loading span{width:5px;height:5px;border-radius:50%;background:var(--primary);opacity:.3;animation:dot 1s infinite}
.ai-loading span:nth-child(2){animation-delay:.2s}
.ai-loading span:nth-child(3){animation-delay:.4s}
@keyframes dot{0%,100%{opacity:.3;transform:scale(1)}50%{opacity:1;transform:scale(1.2)}}
#ai_settings{display:none;position:absolute;top:40px;right:12px;width:min(320px,calc(100%-24px));padding:12px;border:1px solid var(--card-border);border-radius:12px;background:var(--card);box-shadow:0 8px 24px rgba(0,0,0,0.08);gap:10px;flex-wrap:wrap;z-index:10}
#ai_settings.visible{display:flex}
.set-fld{display:flex;flex-direction:column;gap:4px;flex:1;min-width:100px}
.set-fld label{color:var(--muted);font-size:11px;font-weight:500}
.set-fld input,.set-fld select{width:100%;border:1px solid var(--card-border);background:var(--card);color:var(--ink);border-radius:6px;padding:6px 8px;outline:none;font-size:13px}
.set-fld input:focus,.set-fld select:focus{border-color:var(--primary);box-shadow:0 0 0 3px rgba(37,99,235,0.1)}
#ai_input_bar{display:flex;gap:8px;padding:12px 16px;flex-shrink:0;border-top:1px solid var(--divider)}
#ai_input{flex:1;border:1px solid transparent;border-radius:10px;background:var(--bg);color:var(--ink);padding:10px 14px;outline:none;font-size:14px;transition:all .2s}
#ai_input:focus{border-color:var(--primary);background:var(--card);box-shadow:0 0 0 3px rgba(37,99,235,0.1)}
#ai_input::placeholder{color:var(--dim)}
#ai_send_btn{border:none;background:var(--primary);color:#fff;width:36px;height:36px;border-radius:50%;cursor:pointer;display:grid;place-items:center;transition:all .2s;flex-shrink:0;font-size:16px;font-weight:600}
#ai_send_btn:hover{background:var(--primary-h);transform:scale(1.05)}
#ai_send_btn:disabled{opacity:.4;cursor:not-allowed;transform:none}
.ai-hd-right{display:flex;align-items:center;gap:6px}
.ai-status{display:inline-flex;align-items:center;gap:5px;color:var(--muted);font-size:11px}
.dot{width:6px;height:6px;border-radius:50%;background:var(--dim);flex-shrink:0}
.dot.ok{background:var(--success)}
.ico-btn{width:28px;height:28px;display:grid;place-items:center;border:none;border-radius:6px;background:transparent;color:var(--muted);cursor:pointer;font-size:13px;transition:all .15s}
.ico-btn:hover{background:var(--bg);color:var(--ink)}
.ai-cursor{color:var(--primary);animation:blink 1s infinite}
@keyframes blink{0%,100%{opacity:1}50%{opacity:0}}
/* ===== 教学文档 ===== */
.doc-overlay{display:none;position:fixed;inset:0;z-index:100;background:var(--bg)}
.doc-overlay.open{display:flex;flex-direction:column}
.doc-hd{display:flex;align-items:center;justify-content:space-between;padding:12px 24px;background:var(--card);border-bottom:1px solid var(--card-border);flex-shrink:0}
.doc-hd h2{font-size:16px;font-weight:700;color:var(--ink)}
.doc-close{border:none;background:var(--bg);color:var(--muted);width:32px;height:32px;border-radius:8px;cursor:pointer;font-size:18px;display:grid;place-items:center}
.doc-close:hover{background:var(--divider2);color:var(--ink)}
.doc-body{flex:1;overflow:auto;padding:32px 48px;max-width:900px;margin:0 auto}
.doc-body h3{font-size:18px;font-weight:700;color:var(--ink);margin:28px 0 8px;padding-bottom:6px;border-bottom:2px solid var(--primary)}
.doc-body h4{font-size:15px;font-weight:600;color:var(--ink);margin:20px 0 6px}
.doc-body p{font-size:14px;line-height:1.8;color:var(--body);margin:8px 0}
.doc-body table{width:100%;border-collapse:collapse;margin:12px 0;font-size:13px}
.doc-body table th{background:var(--bg);padding:8px 12px;text-align:left;font-weight:600;color:var(--muted);border-bottom:2px solid var(--divider)}
.doc-body table td{padding:8px 12px;border-bottom:1px solid var(--divider2)}
.doc-body blockquote{margin:12px 0;padding:10px 16px;border-left:3px solid var(--primary);background:var(--primary-light);border-radius:0 6px 6px 0;font-size:13px;color:var(--body)}
.doc-body .formula-block{margin:10px 0;padding:10px 14px;background:var(--card);border:1px solid var(--divider2);border-radius:6px;overflow-x:auto}
.doc-body ol,.doc-body ul{padding-left:24px;margin:8px 0}
.doc-body li{font-size:14px;line-height:1.8;color:var(--body);margin:4px 0}
.doc-body code{font-family:var(--mono);font-size:12px;background:var(--bg);padding:1px 4px;border-radius:3px}
.doc-disclaimer{margin-bottom:20px;padding:12px 16px;background:var(--warning-light);border:1px solid #fde68a;border-radius:8px;font-size:12px;color:#92400e;line-height:1.6}
.doc-step{display:flex;align-items:center;gap:8px;margin:4px 0;font-size:14px}
.doc-step .arrow{color:var(--primary);font-weight:700}
.katex{font-size:1em}
.binding-row{display:flex;justify-content:space-between;align-items:center;padding:4px 0;font-family:var(--mono);font-size:12px}
.curve-hd{cursor:pointer;display:flex;align-items:center;gap:8px}
.curve-summary{font-size:11px;color:var(--muted);margin-bottom:4px}
.curve-table{width:100%;border-collapse:collapse;font-size:11px}
.curve-table th{color:var(--dim);font-weight:600;text-align:left;padding:2px 4px;font-size:10px}
.curve-table td{padding:2px 4px;font-family:var(--mono);border-bottom:1px solid var(--divider2)}
.curve-table td:last-child{text-align:right}
.dist-info{font-size:11px;color:var(--muted);margin-bottom:8px}
</style>
</head>
<body>
<div class="app-shell">
<header class="topbar">
<div class="brand">
<div class="brand-mark"><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.4" d="M13 3L4 14h7l-1 7 10-12h-7l0-6z"></path></svg></div>
<div>
<h1>SCED / LMP 教学系统</h1>
<p>5 节点直流潮流 · 安全经济调度 · 节点电价拆解</p>
</div>
</div>
<span class="live-badge"><span class="live-dot"></span>实时仿真</span>
</header>
<main class="workspace">
<!-- Left: Parameters -->
<section class="panel">
<div class="panel-hd"><h2><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6h10M4 6h2m4 6h10M4 12h2m4 6h10M4 18h2"></path></svg>申报与边界参数</h2></div>
<div class="panel-bd" id="param_bd">
<div class="sec-label">出力-成本曲线</div>
<div class="dist-info">煤电 16台 (A/B/D×4, E/F×2) · 燃气 16台 (A/B/D×4, E/F×2)</div>
<div class="gen-card">
<div class="gen-hd curve-hd" onclick="toggleCurve('coal')">
<span class="gen-name">煤电报价</span><span class="gen-desc gen-desc-low">单机 300~600MW</span>
<span class="gen-summary" id="coal_summary"></span>
<span class="gen-toggle" id="coal_toggle">▶</span>
</div>
<div class="gen-body" id="coal_body" style="display:none">
<table class="curve-table" id="coal_table"></table>
<textarea class="gen-json" id="coal_json" rows="6" spellcheck="false">[{"mw":300,"price":230},{"mw":33,"price":250},{"mw":34,"price":270},{"mw":33,"price":290},{"mw":33,"price":310},{"mw":34,"price":330},{"mw":33,"price":350},{"mw":33,"price":370},{"mw":34,"price":390},{"mw":33,"price":410}]</textarea>
<button class="gen-btn gen-btn-apply" onclick="event.stopPropagation();applyCurve('coal');runSCED()">应用</button>
</div>
</div>
<div class="gen-card">
<div class="gen-hd curve-hd" onclick="toggleCurve('gas')">
<span class="gen-name">燃气报价</span><span class="gen-desc gen-desc-high">单机 150~300MW</span>
<span class="gen-summary" id="gas_summary"></span>
<span class="gen-toggle" id="gas_toggle">▶</span>
</div>
<div class="gen-body" id="gas_body" style="display:none">
<table class="curve-table" id="gas_table"></table>
<textarea class="gen-json" id="gas_json" rows="6" spellcheck="false">[{"mw":150,"price":500},{"mw":15,"price":520},{"mw":15,"price":540},{"mw":15,"price":560},{"mw":15,"price":580},{"mw":15,"price":600},{"mw":15,"price":620},{"mw":15,"price":640},{"mw":15,"price":660},{"mw":30,"price":680}]</textarea>
<button class="gen-btn gen-btn-apply" onclick="event.stopPropagation();applyCurve('gas');runSCED()">应用</button>
</div>
</div>
<div class="sec-label">对手策略 (30台机组)</div>
<div style="display:flex;gap:4px;margin-bottom:4px;align-items:center">
<select id="comp_mode" style="flex:1;border:1px solid var(--card-border);border-radius:6px;padding:4px;font-size:12px" onchange="onCompModeChange()">
<option value="manual">手动加成</option>
<option value="auto">自动策略</option>
</select>
</div>
<div id="comp_manual">
<div class="fgrid">
<div class="fld"><label>煤加价%</label><input id="comp_coal_markup" type="number" value="0" min="0" max="100" step="1"><input id="slider_comp_coal_markup" type="range" min="0" max="100" value="0"></div>
<div class="fld"><label>气加价%</label><input id="comp_gas_markup" type="number" value="0" min="0" max="100" step="1"><input id="slider_comp_gas_markup" type="range" min="0" max="100" value="0"></div>
</div>
</div>
<div id="comp_auto" style="display:none">
<div style="font-size:11px;color:var(--muted);line-height:1.5">
根据系统负荷自动调整:<br>
负荷率>80%→加20% · >60%→加10%<br>
负荷率>40%→加5% · ≤40%→不加价<br>
各台±5%随机扰动
</div>
</div>
<div id="comp_player_bid">
<div class="sec-label">选手所在节点</div>
<div style="display:flex;gap:6px;align-items:center;margin-bottom:8px">
<select id="player_node" style="flex:1;border:1px solid var(--card-border);border-radius:6px;padding:5px 6px;font-size:12px" onchange="onPlayerChange()">
<option value="">不参与(仅查看)</option>
<option value="a">A 节点</option><option value="b">B 节点</option>
<option value="d">D 节点</option><option value="e">E 节点</option><option value="f">F 节点</option>
</select>
<select id="player_group" style="flex:1;border:1px solid var(--card-border);border-radius:6px;padding:5px 6px;font-size:12px" onchange="onPlayerGroupChange()">
<option value="0">机组组1</option>
</select>
</div>
<div class="sec-label">我的报价曲线</div>
<div class="gen-card">
<div class="gen-hd curve-hd" onclick="toggleCurve('mycoal')">
<span class="gen-name">煤电报价</span><span class="gen-desc gen-desc-low">我的机组</span>
<span class="gen-summary" id="mycoal_summary"></span>
<span class="gen-toggle" id="mycoal_toggle">▶</span>
</div>
<div class="gen-body" id="mycoal_body" style="display:none">
<table class="curve-table" id="mycoal_table"></table>
<textarea class="gen-json" id="mycoal_json" rows="6" spellcheck="false"></textarea>
<button class="gen-btn gen-btn-apply" onclick="event.stopPropagation();applyPlayerBid('coal');runSCED()">应用</button>
</div>
</div>
<div class="gen-card">
<div class="gen-hd curve-hd" onclick="toggleCurve('mygas')">
<span class="gen-name">燃气报价</span><span class="gen-desc gen-desc-high">我的机组</span>
<span class="gen-summary" id="mygas_summary"></span>
<span class="gen-toggle" id="mygas_toggle">▶</span>
</div>
<div class="gen-body" id="mygas_body" style="display:none">
<table class="curve-table" id="mygas_table"></table>
<textarea class="gen-json" id="mygas_json" rows="6" spellcheck="false"></textarea>
<button class="gen-btn gen-btn-apply" onclick="event.stopPropagation();applyPlayerBid('gas');runSCED()">应用</button>
</div>
<div class="sec-label">机组成本参数</div>
<div class="fgrid">
<div class="fld"><label>煤空载 元/h</label><input id="coal_noload" type="number" value="23635"></div>
<div class="fld"><label>煤启动 元</label><input id="coal_startup" type="number" value="600000"></div>
<div class="fld"><label>气空载 元/h</label><input id="gas_noload" type="number" value="33124"></div>
<div class="fld"><label>气启动 元</label><input id="gas_startup" type="number" value="12000"></div>
</div>
<div class="sec-label">输电线路约束</div>
<div class="line-card">
<div class="gen-hd"><span class="gen-name">AB 通道</span><span class="gen-desc">A ↔ B</span></div>
<div class="fgrid">
<div class="fld"><label>限额 MW</label><input id="limit_ab" type="number" value="60"><input id="slider_limit_ab" type="range" min="0" max="300" value="60"></div>
<div class="fld"><label>电抗 p.u.</label><input id="x_ab" type="number" step="0.1" min="0.1" value="1.0"></div>
</div>
</div>
<div class="line-card">
<div class="gen-hd"><span class="gen-name">AD 通道</span><span class="gen-desc">A ↔ D</span></div>
<div class="fgrid">
<div class="fld"><label>限额 MW</label><input id="limit_ad" type="number" value="60"><input id="slider_limit_ad" type="range" min="0" max="300" value="60"></div>
<div class="fld"><label>电抗 p.u.</label><input id="x_ad" type="number" step="0.1" min="0.1" value="1.0"></div>
</div>
</div>
<div class="line-card">
<div class="gen-hd"><span class="gen-name">BD 通道</span><span class="gen-desc">B ↔ D</span></div>
<div class="fgrid">
<div class="fld"><label>限额 MW</label><input id="limit_bd" type="number" value="60"><input id="slider_limit_bd" type="range" min="0" max="300" value="60"></div>
<div class="fld"><label>电抗 p.u.</label><input id="x_bd" type="number" step="0.1" min="0.1" value="1.0"></div>
</div>
</div>
<div class="line-card">
<div class="gen-hd"><span class="gen-name">AE 通道</span><span class="gen-desc">A ↔ E</span></div>
<div class="fgrid">
<div class="fld"><label>限额 MW</label><input id="limit_ae" type="number" value="60"><input id="slider_limit_ae" type="range" min="0" max="300" value="60"></div>
<div class="fld"><label>电抗 p.u.</label><input id="x_ae" type="number" step="0.1" min="0.1" value="1.0"></div>
</div>
</div>
<div class="line-card">
<div class="gen-hd"><span class="gen-name">BE 通道</span><span class="gen-desc">B ↔ E</span></div>
<div class="fgrid">
<div class="fld"><label>限额 MW</label><input id="limit_be" type="number" value="60"><input id="slider_limit_be" type="range" min="0" max="300" value="60"></div>
<div class="fld"><label>电抗 p.u.</label><input id="x_be" type="number" step="0.1" min="0.1" value="1.0"></div>
</div>
</div>
<div class="line-card">
<div class="gen-hd"><span class="gen-name">DE 通道</span><span class="gen-desc">D ↔ E</span></div>
<div class="fgrid">
<div class="fld"><label>限额 MW</label><input id="limit_de" type="number" value="60"><input id="slider_limit_de" type="range" min="0" max="300" value="60"></div>
<div class="fld"><label>电抗 p.u.</label><input id="x_de" type="number" step="0.1" min="0.1" value="1.0"></div>
</div>
</div>
<div class="line-card">
<div class="gen-hd"><span class="gen-name">EF 通道</span><span class="gen-desc">E ↔ F</span></div>
<div class="fgrid">
<div class="fld"><label>限额 MW</label><input id="limit_ef" type="number" value="10000"><input id="slider_limit_ef" type="range" min="0" max="20000" value="10000"></div>
<div class="fld"><label>电抗 p.u.</label><input id="x_ef" type="number" step="0.1" min="0.1" value="1.0"></div>
</div>
</div>
</div>
<div class="sec-label">系统负荷 <select id="load_mode" onchange="switchLoadMode()" style="border:1px solid var(--card-border);border-radius:4px;padding:2px 6px;font-size:11px;margin-left:4px"><option value="loose">宽松</option><option value="tight">紧张</option></select></div>
<textarea id="load_series" rows="5" style="width:100%;border:1px solid var(--card-border);border-radius:6px;font-family:var(--mono);font-size:11px;padding:6px;resize:vertical" spellcheck="false">5399.3,5365.9,5289.9,5204,5136.5,5058.4,5005.6,4960.4,4919,4877.5,4847.1,4815.3,4795.8,4777.7,4760.2,4751.6,4750,4758.4,4770,4789.5,4843.9,4928.8,5021.8,5086.2,5289,5428.7,5691.3,5912.3,6143.5,6468.4,6734.2,6989.6,7131.8,7308.6,7385.4,7410.2,7434.5,7462.3,7479.2,7493,7500,7474.3,7406.5,7086.4,6930.6,6688.4,6517.9,6538.9,6703.3,6895.8,7038.9,7037.8,7037.7,7002.4,7074.0,7005.5,7017.2,7072.8,7080.9,7019.4,7072.5,7113.0,7171.8,7228.3,7325.5,7379.9,7405.1,7405.2,7405.4,7379.9,7361.3,7325.3,7293.1,7255.0,7210.1,7163.6,7104.7,7053.3,6997.6,6942.8,6906.2,6871.9,6832.5,6804.5,6762.8,6677.5,6578.2,6508.2,6549.5,6364.3,6221.3,6072.3,5885.7,5701.7,5600.3,5470.0</textarea>
<div class="gen-row" style="margin-bottom:4px">
<button class="gen-btn gen-btn-default" onclick="fillDemoLoad()" style="flex:1">📋 示例</button>
<button class="gen-btn gen-btn-apply" onclick="batchSolve()" style="flex:1" id="btn_batch">▶ 计算96时段</button>
<button class="gen-btn gen-btn-default" onclick="togglePause()" style="flex:0" id="btn_pause" disabled>⏸</button>
</div>
<div style="display:flex;align-items:center;gap:4px;margin-bottom:4px;font-size:11px;color:var(--muted)">
<span>⏱</span>
<select id="timeout_mins" style="border:1px solid var(--card-border);border-radius:4px;padding:2px 4px;font-size:11px">
<option value="1">1</option>
<option value="2">2</option>
<option value="3" selected>3</option>
<option value="5">5</option>
<option value="10">10</option>
</select>
<span>分钟</span>
</div>
<div id="batch_progress" style="font-size:11px;color:var(--muted)"></div>
<div id="batch_bar_wrap" style="display:none;margin:4px 0;height:6px;background:#e5e7eb;border-radius:3px;overflow:hidden">
<div id="batch_bar_fill" style="height:100%;width:0%;background:linear-gradient(90deg,#2563eb,#3b82f6);border-radius:3px;transition:width .4s ease"></div>
</div>
<div id="batch_time" style="font-size:10px;color:var(--dim);display:none"></div>
<button class="gen-btn gen-btn-default" onclick="exportCSV()" id="btn_export" style="width:100%;display:none">📥 导出结果 CSV</button>
<svg id="load_chart" viewBox="0 0 340 80" style="width:100%;height:80px;margin:4px 0;border:1px solid var(--divider2);border-radius:6px">
<polyline id="load_chart_line" fill="none" stroke="#2563eb" stroke-width="1.5"/>
</svg>
<div class="sec-label">查看时段</div>
<div style="display:flex;align-items:center;gap:6px">
<input id="period_slider" type="range" min="0" max="95" value="0" oninput="onPeriodChange(this.value)" style="flex:1">
<span id="period_label" style="font-size:12px;font-weight:600;color:var(--ink);min-width:50px;text-align:right">00:00</span>
</div>
<input type="hidden" id="total_load" value="1200">
<div class="fgrid">
<div class="fld"><label>正备用 MW</label><input id="pos_reserve" type="number" value="600"><input id="slider_pos_reserve" type="range" min="0" max="2000" value="600"></div>
<div class="fld"><label>负备用 MW</label><input id="neg_reserve" type="number" value="112.5"><input id="slider_neg_reserve" type="range" min="0" max="500" value="112.5" step="0.5"></div>
</div>
</div>
</section>
<!-- Center: Topology + Results -->
<section class="center-stack">
<div class="panel" style="flex:0 0 50%;min-height:0">
<div class="panel-hd"><h2><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7l8-4 8 4-8 4-8-4zm0 5l8 4 8-4M4 17l8 4 8-4"></path></svg>物理输网潮流拓扑</h2></div>
<div class="topology" style="flex:1;min-height:0">
<svg id="network_svg" viewBox="0 0 880 480">
<defs>
<filter id="nodeShadow"><feDropShadow dx="0" dy="2" stdDeviation="4" flood-opacity="0.08"/></filter>
</defs>
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<circle cx="10" cy="10" r="0.5" fill="#e2e8f0"/>
</pattern>
<rect width="880" height="480" fill="url(#grid)" opacity="0.5"/>
<!-- Static outline lines -->
<g opacity=".5">
<line x1="720" y1="240" x2="480" y2="60" stroke="#cbd5e1" stroke-width="1.5"/>
<line x1="720" y1="240" x2="480" y2="420" stroke="#cbd5e1" stroke-width="1.5"/>
<line x1="480" y1="60" x2="480" y2="420" stroke="#cbd5e1" stroke-width="1.5"/>
<line x1="720" y1="240" x2="330" y2="240" stroke="#cbd5e1" stroke-width="1.5"/>
<line x1="480" y1="60" x2="330" y2="240" stroke="#cbd5e1" stroke-width="1.5"/>
<line x1="480" y1="420" x2="330" y2="240" stroke="#cbd5e1" stroke-width="1.5"/>
<line x1="330" y1="240" x2="10" y2="240" stroke="#cbd5e1" stroke-width="1.5"/>
</g>
<!-- Dynamic flow lines -->
<line id="flow_ab" x1="720" y1="240" x2="480" y2="60" class="flow-line" stroke="#cbd5e1" stroke-width="2"/>
<line id="flow_ad" x1="720" y1="240" x2="480" y2="420" class="flow-line" stroke="#cbd5e1" stroke-width="2"/>
<line id="flow_bd" x1="480" y1="60" x2="480" y2="420" class="flow-line" stroke="#cbd5e1" stroke-width="2"/>
<line id="flow_ae" x1="720" y1="240" x2="330" y2="240" class="flow-line" stroke="#cbd5e1" stroke-width="2"/>
<line id="flow_be" x1="480" y1="60" x2="330" y2="240" class="flow-line" stroke="#cbd5e1" stroke-width="2"/>
<line id="flow_de" x1="480" y1="420" x2="330" y2="240" class="flow-line" stroke="#cbd5e1" stroke-width="2"/>
<line id="flow_ef" x1="330" y1="240" x2="10" y2="240" class="flow-line" stroke="#cbd5e1" stroke-width="2"/>
<!-- Flow arrows -->
<polygon id="arrow_ab" points="0,-7 12,0 0,7" fill="#cbd5e1" transform="translate(600,150)"/>
<polygon id="arrow_ad" points="0,-7 12,0 0,7" fill="#cbd5e1" transform="translate(600,330)"/>
<polygon id="arrow_bd" points="0,-7 12,0 0,7" fill="#cbd5e1" transform="translate(480,240)"/>
<polygon id="arrow_ae" points="0,-7 12,0 0,7" fill="#cbd5e1" transform="translate(525,240)"/>
<polygon id="arrow_be" points="0,-7 12,0 0,7" fill="#cbd5e1" transform="translate(405,150)"/>
<polygon id="arrow_de" points="0,-7 12,0 0,7" fill="#cbd5e1" transform="translate(405,330)"/>
<polygon id="arrow_ef" points="0,-7 12,0 0,7" fill="#cbd5e1" transform="translate(170,240)"/>
<!-- Line labels -->
<g transform="translate(590,130)"><text text-anchor="middle" fill="#1e293b" font-size="10" font-weight="600" font-family="var(--mono)">L<tspan font-size="7" baseline-shift="sub">AB</tspan>: <tspan id="flow_txt_ab">0</tspan>/<tspan id="flow_lim_ab">100</tspan></text></g>
<g transform="translate(590,350)"><text text-anchor="middle" fill="#1e293b" font-size="10" font-weight="600" font-family="var(--mono)">L<tspan font-size="7" baseline-shift="sub">AD</tspan>: <tspan id="flow_txt_ad">0</tspan>/<tspan id="flow_lim_ad">100</tspan></text></g>
<g transform="translate(480,230)"><text text-anchor="end" fill="#1e293b" font-size="10" font-weight="600" font-family="var(--mono)">L<tspan font-size="7" baseline-shift="sub">BD</tspan>: <tspan id="flow_txt_bd">0</tspan>/<tspan id="flow_lim_bd">100</tspan></text></g>
<g transform="translate(525,255)"><text text-anchor="start" fill="#1e293b" font-size="10" font-weight="600" font-family="var(--mono)">L<tspan font-size="7" baseline-shift="sub">AE</tspan>: <tspan id="flow_txt_ae">0</tspan>/<tspan id="flow_lim_ae">100</tspan></text></g>
<g transform="translate(405,130)"><text text-anchor="middle" fill="#1e293b" font-size="10" font-weight="600" font-family="var(--mono)">L<tspan font-size="7" baseline-shift="sub">BE</tspan>: <tspan id="flow_txt_be">0</tspan>/<tspan id="flow_lim_be">100</tspan></text></g>
<g transform="translate(405,350)"><text text-anchor="middle" fill="#1e293b" font-size="10" font-weight="600" font-family="var(--mono)">L<tspan font-size="7" baseline-shift="sub">DE</tspan>: <tspan id="flow_txt_de">0</tspan>/<tspan id="flow_lim_de">100</tspan></text></g>
<g transform="translate(170,225)"><text text-anchor="middle" fill="#1e293b" font-size="10" font-weight="600" font-family="var(--mono)">L<tspan font-size="7" baseline-shift="sub">EF</tspan>: <tspan id="flow_txt_ef">0</tspan>/<tspan id="flow_lim_ef">100</tspan></text></g>
<!-- Node A -->
<g transform="translate(720,240)" filter="url(#nodeShadow)">
<circle r="28" fill="#ffffff" stroke="#cbd5e1" stroke-width="2"/>
<text text-anchor="middle" y="-1" fill="#1e293b" font-weight="700" font-size="14">A</text>
<text id="lmp_lbl_a" text-anchor="middle" y="13" fill="#64748b" font-size="9" font-weight="500">---</text>
<rect x="-88" y="-14" width="60" height="16" rx="5" fill="#f0fdf4" stroke="#bbf7d0"/>
<text x="-58" y="-2" text-anchor="middle" fill="#10b981" font-size="10" font-weight="600">G<tspan font-size="7" baseline-shift="sub">A</tspan>:<tspan id="node_a_gen">0</tspan>MW</text>
<rect x="28" y="-14" width="64" height="16" rx="5" fill="#eff6ff" stroke="#bfdbfe"/>
<text x="60" y="-2" text-anchor="middle" fill="#2563eb" font-size="10" font-weight="600">L<tspan font-size="7" baseline-shift="sub">A</tspan>:<tspan id="node_a_load">0</tspan>MW</text>
</g>
<!-- Node B -->
<g transform="translate(480,60)" filter="url(#nodeShadow)">
<circle r="28" fill="#ffffff" stroke="#cbd5e1" stroke-width="2"/>
<text text-anchor="middle" y="-1" fill="#1e293b" font-weight="700" font-size="14">B</text>
<text id="lmp_lbl_b" text-anchor="middle" y="13" fill="#64748b" font-size="9" font-weight="500">---</text>
<rect x="-88" y="-50" width="60" height="16" rx="5" fill="#f0fdf4" stroke="#bbf7d0"/>
<text x="-58" y="-38" text-anchor="middle" fill="#10b981" font-size="10" font-weight="600">G<tspan font-size="7" baseline-shift="sub">B</tspan>:<tspan id="node_b_gen">0</tspan>MW</text>
<rect x="28" y="-50" width="64" height="16" rx="5" fill="#eff6ff" stroke="#bfdbfe"/>
<text x="60" y="-38" text-anchor="middle" fill="#2563eb" font-size="10" font-weight="600">L<tspan font-size="7" baseline-shift="sub">B</tspan>:<tspan id="node_b_load">0</tspan>MW</text>
</g>
<!-- Node D -->
<g transform="translate(480,420)" filter="url(#nodeShadow)">
<circle r="28" fill="#ffffff" stroke="#cbd5e1" stroke-width="2"/>
<text text-anchor="middle" y="-1" fill="#1e293b" font-weight="700" font-size="14">D</text>
<text id="lmp_lbl_d" text-anchor="middle" y="13" fill="#64748b" font-size="9" font-weight="500">---</text>
<rect x="-88" y="32" width="60" height="16" rx="5" fill="#f0fdf4" stroke="#bbf7d0"/>
<text x="-58" y="44" text-anchor="middle" fill="#10b981" font-size="10" font-weight="600">G<tspan font-size="7" baseline-shift="sub">D</tspan>:<tspan id="node_d_gen">0</tspan>MW</text>
<rect x="28" y="32" width="64" height="16" rx="5" fill="#eff6ff" stroke="#bfdbfe"/>
<text x="60" y="44" text-anchor="middle" fill="#2563eb" font-size="10" font-weight="600">L<tspan font-size="7" baseline-shift="sub">D</tspan>:<tspan id="node_d_load">0</tspan>MW</text>
</g>
<!-- Node E (reference) -->
<g transform="translate(330,240)" filter="url(#nodeShadow)">
<circle r="28" fill="#ffffff" stroke="#e2e8f0" stroke-width="2" stroke-dasharray="4 2"/>
<text text-anchor="middle" y="-1" fill="#1e293b" font-weight="700" font-size="14">E</text>
<text id="lmp_lbl_e" text-anchor="middle" y="13" fill="#64748b" font-size="9" font-weight="500">---</text>
<text text-anchor="middle" y="46" fill="#94a3b8" font-size="9" font-style="italic">平衡参考节点</text>
<rect x="-88" y="-14" width="60" height="16" rx="5" fill="#f0fdf4" stroke="#bbf7d0"/>
<text x="-58" y="-2" text-anchor="middle" fill="#10b981" font-size="10" font-weight="600">G<tspan font-size="7" baseline-shift="sub">E</tspan>:<tspan id="node_e_gen">0</tspan>MW</text>
<rect x="28" y="-14" width="64" height="16" rx="5" fill="#eff6ff" stroke="#bfdbfe"/>
<text x="60" y="-2" text-anchor="middle" fill="#2563eb" font-size="10" font-weight="600">L<tspan font-size="7" baseline-shift="sub">E</tspan>:<tspan id="node_e_load">0</tspan>MW</text>
</g>
<!-- Node F -->
<g transform="translate(10,240)" filter="url(#nodeShadow)">
<circle r="28" fill="#ffffff" stroke="#cbd5e1" stroke-width="2"/>
<text text-anchor="middle" y="-1" fill="#1e293b" font-weight="700" font-size="14">F</text>
<text id="lmp_lbl_f" text-anchor="middle" y="13" fill="#64748b" font-size="9" font-weight="500">---</text>
<rect x="-88" y="-14" width="60" height="16" rx="5" fill="#f0fdf4" stroke="#bbf7d0"/>
<text x="-58" y="-2" text-anchor="middle" fill="#10b981" font-size="10" font-weight="600">G<tspan font-size="7" baseline-shift="sub">F</tspan>:<tspan id="node_f_gen">0</tspan>MW</text>
<rect x="28" y="-14" width="64" height="16" rx="5" fill="#eff6ff" stroke="#bfdbfe"/>
<text x="60" y="-2" text-anchor="middle" fill="#2563eb" font-size="10" font-weight="600">L<tspan font-size="7" baseline-shift="sub">F</tspan>:<tspan id="node_f_load">0</tspan>MW</text>
</g>
</svg>
<div id="error_overlay" class="error-overlay hidden">
<div style="color:var(--error);font-size:28px;line-height:1">!</div>
<div style="font-weight:700;font-size:14px;color:var(--ink)">SCED 安全出清失败</div>
<div style="color:var(--dim);font-size:12px" id="error_desc">请放宽输电断面或减少负荷。</div>
</div>
</div>
</div>
<div class="panel" style="flex:1;min-height:0">
<div class="panel-hd"><h2><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 19V5m0 14h16M8 17v-5m4 5V7m4 10v-8"></path></svg>出清数据透视</h2>
<button class="btn-fullscreen" onclick="toggleResultFull()" id="btn_result_full">全屏查看</button></div>
<div class="tabs">
<button class="tab-btn active" onclick="switchTab(0)">总体出清情况</button>
<button class="tab-btn" onclick="switchTab(1)">各节点出清</button>
<button class="tab-btn" onclick="switchTab(2)">潮流计算</button>
<button class="tab-btn" onclick="switchTab(3)">线路影子价格</button>
<button class="tab-btn" onclick="toggleDoc()" style="margin-left:auto">📖 教学文档</button>
</div>
<div class="panel-bd result-bd">
<div class="tab-content active" id="tab_0"></div>
<div class="tab-content" id="tab_1"></div>
<div class="tab-content" id="tab_2"></div>
<div class="tab-content" id="tab_3"></div>
</div>
</div>
</section>
<!-- Right: AI 教学助教 -->
<section class="panel ai-panel">
<div class="panel-hd">
<h2><svg fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 10h8M8 14h5m6 6l-3.8-3.8A7.5 7.5 0 1018 11.5a7.5 7.5 0 00-2.8-5.8"></path></svg>AI 教学助教</h2>
<div class="ai-hd-right">
<span class="ai-status"><span id="ai_dot" class="dot"></span><span id="ai_stxt">未配置</span></span>
<button class="ico-btn" onclick="toggleAiSettings()" title="配置">⚙</button>
<button class="ico-btn" onclick="clearAiChat()" title="重置">↺</button>
</div>
</div>
<div id="ai_body">
<div id="ai_messages"><div style="color:var(--dim);font-size:13px;padding:8px 0">配置 API 后,可基于当前出清结果进行教学问答。</div></div>
<div id="ai_input_bar">
<input id="ai_input" type="text" placeholder="输入问题,如:为什么 B 点 LMP 更高?">
<button id="ai_send_btn" type="button" onclick="sendAiMessage()">→</button>
</div>
</div>
<div id="ai_settings">
<div class="set-fld"><label>Base URL</label><input id="ai_base_url" type="text" placeholder="https://api.deepseek.com"></div>
<div class="set-fld"><label>API Key</label><input id="ai_api_key" type="password" placeholder="sk-..."></div>
<div class="set-fld"><label>模型</label><input id="ai_model" type="text" value="deepseek-chat"></div>
<div class="set-fld"><label>温度</label><input id="ai_temp" type="number" step="0.1" min="0" max="2" value="0.2"></div>
<div class="set-fld"><label>记住 Key</label><select id="ai_remember_key"><option value="0">仅本次</option><option value="1">存入浏览器</option></select></div>
</div>
</section>
</main>
</div>
<!-- ===== 教学文档 Overlay ===== -->
<div id="doc_overlay" class="doc-overlay">
<div class="doc-hd"><h2>📖 SCED / LMP 教学文档</h2><button class="doc-close" onclick="toggleDoc()">✕</button></div>
<div class="doc-body">
<div class="doc-disclaimer">⚠ 本文档基于当前系统默认参数编写,仅供教学参考。实际电力市场运行请以各省级电力交易中心发布的正式规则为准。</div>
<h3>第一章:从交流电到 DC 潮流——为什么要简化</h3>
<p><strong>本章核心:</strong>电力系统精确模型是复杂的非线性交流方程,DC 潮流通过三个物理合理的假设将其线性化,使大规模计算成为可能。</p>
<h4>1.1 交流潮流的精确方程</h4>
<p>本节展示交流系统中节点功率注入的精确数学表达式。节点 $i$ 的复功率注入方程为:</p>
<div class="formula-block">$$\tilde{S}_i = P_i + jQ_i = \tilde{V}_i \cdot \tilde{I}_i^*$$</div>
<p>其中每个符号的含义如下:</p>
<ul>
<li>$\tilde{S}_i$(节点 $i$ 的复功率,单位 MVA)是复数,头顶弯号表示该量为复数形式,与普通实数区分</li>
<li>$P_i$(节点 $i$ 的有功功率,单位 MW)是 $\tilde{S}_i$ 的实部,代表实际做功的功率</li>
<li>$Q_i$(节点 $i$ 的无功功率,单位 Mvar)是 $\tilde{S}_i$ 的虚部,用于维持电压水平</li>
<li>$\tilde{V}_i$(节点 $i$ 的复电压,单位 p.u.)$= V_i e^{j\theta_i}$,其中 $V_i$ 为幅值,$\theta_i$ 为相角</li>
<li>$\tilde{I}_i^*$(节点 $i$ 的复电流的共轭,单位 p.u.),共轭运算将虚部取反</li>
</ul>
<blockquote>工程师用<strong>相量</strong>(Phasor)将正弦波表示为复数,把时域微分方程转化为复数代数方程,这是电力系统分析的数学基础。</blockquote>
<h4>1.2 精确有功功率的完整表达式</h4>
<p>本节从复功率方程出发,推导出精确有功功率的表达式。从 $\tilde{I} = Y \cdot \tilde{V}$ 展开,得到节点 $i$ 的精确有功功率:</p>
<div class="formula-block">$$P_i = \sum_j V_i V_j \left( G_{ij} \cos\theta_{ij} + B_{ij} \sin\theta_{ij} \right)$$</div>
<p>其中:</p>
<ul>
<li>$V_i$(节点 $i$ 的电压幅值,单位 p.u.)和 $V_j$(节点 $j$ 的电压幅值,单位 p.u.)是节点电压的有效值</li>
<li>$G_{ij}$(节点 $i$ 与 $j$ 之间的电导,单位 p.u.)反映线路电阻损耗</li>
<li>$B_{ij}$(节点 $i$ 与 $j$ 之间的电纳,单位 p.u.)反映线路电抗特性</li>
<li>$\theta_{ij}$(节点 $i$ 与 $j$ 之间的相角差,单位 rad)$= \theta_i - \theta_j$</li>
</ul>
<blockquote>这个方程包含电压幅值、相角差、三角函数、电导和电纳共五类变量,是一个<strong>非线性方程</strong>,无法直接用线性代数求解。</blockquote>
<h4>1.3 DC 潮流的三个简化假设</h4>
<p>本节说明工程师如何通过三个物理合理的假设,将非线性方程线性化:</p>
<table>
<tr><th>简化步骤</th><th>被忽略的物理量</th><th>近似处理</th><th>合理性依据</th></tr>
<tr><td>第一步</td><td>电阻 $R_{ij}$</td><td>令电导 $G_{ij} \approx 0$</td><td>高压线路满足 $R \ll X$,电阻远小于电抗</td></tr>
<tr><td>第二步</td><td>电压幅值变化</td><td>令 $V_i \approx 1$ p.u.</td><td>系统电压维持在额定值附近,偏差很小</td></tr>
<tr><td>第三步</td><td>相角非线性</td><td>令 $\sin\theta_{ij} \approx \theta_{ij}$</td><td>正常运行时相角差很小($< 30°$),正弦近似线性</td></tr>
</table>
<h4>1.4 简化后的 DC 潮流方程</h4>
<p>经过三步简化,精确方程变为:</p>
<div class="formula-block">$$P_i = \sum_j \frac{\theta_i - \theta_j}{x_{ij}}$$</div>
<p>其中 $P_i$(节点 $i$ 的净注入有功功率,单位 MW),$\theta_i$(节点 $i$ 的电压相角,单位 rad),$x_{ij}$(线路 $i$-$j$ 的电抗,单位 p.u.)。分子 $\theta_i - \theta_j$ 是相角差,推动功率流动;分母 $x_{ij}$ 是线路电抗,阻碍功率流动。</p>
<h3>第二章:DC 潮流计算——从矩阵到线路潮流</h3>
<p><strong>本章核心:</strong>将单个节点的功率方程组装成矩阵方程,通过降阶和求逆得到各节点相角,最终计算出每条线路上的功率流。</p>
<h4>2.1 节点导纳矩阵 $B'$</h4>
<p>本节说明如何从线路参数构造节点导纳矩阵。对于三节点系统(A、B、C),$B'$ 矩阵的构造规则如下:</p>
<ul>
<li>对角元 $B'_{ii} = \sum_{j} \frac{1}{x_{ij}}$(与节点 $i$ 相连的所有线路导纳之和)</li>
<li>非对角元 $B'_{ij} = -\frac{1}{x_{ij}}$(节点 $i$ 与 $j$ 之间直接相连的导纳的负值)</li>
</ul>
<p>以系统默认参数($x_{AB}=1, x_{BC}=1, x_{AC}=1$ p.u.)为例:</p>
<div class="formula-block">$$B' = \begin{pmatrix} 2 & -1 & -1 \\ -1 & 2 & -1 \\ -1 & -1 & 2 \end{pmatrix}$$</div>
<p>$B'_{AA}=2$ 表示节点 A 与 B、C 两条线路相连,导纳之和为 $1/1+1/1=2$。$B'_{AB}=-1$ 表示 A 与 B 之间线路导纳为 $1/1=1$,取负值。</p>
<h4>2.2 矩阵形式的功率方程</h4>
<p>本节将节点方程组装为矩阵形式:</p>
<div class="formula-block">$$P = B' \cdot \theta$$</div>
<p>其中 $P$(节点净注入功率向量,单位 MW)$= [P_A, P_B, P_C]^T$,$B'$(节点导纳矩阵,单位 p.u.),$\theta$(节点相角向量,单位 rad)$= [\theta_A, \theta_B, \theta_C]^T$。</p>
<h4>2.3 降阶处理——去掉平衡参考节点</h4>
<p>本节说明为什么要降阶以及如何降阶。节点 C 被选为<strong>平衡参考节点</strong>(Slack Bus),其相角 $\theta_C$ 被强制固定为 0。因此,C 对应的行和列从方程组中移除。</p>
<blockquote>三个原始方程线性相关(三行相加为零),只有 2 个独立方程。降阶后,未知数变为 $\theta_A$ 和 $\theta_B$,方程组有唯一解。</blockquote>
<p>降阶后的 2×2 矩阵:</p>
<div class="formula-block">$$B'_{\text{reduced}} = \begin{pmatrix} 2 & -1 \\ -1 & 2 \end{pmatrix}$$</div>
<h4>2.4 矩阵求逆与相角求解</h4>
<p>本节展示矩阵求逆的完整代数过程。2×2 矩阵求逆公式:</p>
<div class="formula-block">$$\theta = (B'_{\text{reduced}})^{-1} \cdot P$$</div>
<p>行列式:$\det = 2 \times 2 - (-1) \times (-1) = 3$</p>
<p>逆矩阵元素:$(B'^{-1})_{AA} = 2/3 \approx 0.6667$,$(B'^{-1})_{BB} = 2/3 \approx 0.6667$,$(B'^{-1})_{AB} = 1/3 \approx 0.3333$</p>
<div class="formula-block">$$(B'_{\text{reduced}})^{-1} = \frac{1}{3}\begin{pmatrix} 2 & 1 \\ 1 & 2 \end{pmatrix} = \begin{pmatrix} 0.6667 & 0.3333 \\ 0.3333 & 0.6667 \end{pmatrix}$$</div>
<h4>2.5 线路潮流计算</h4>
<p>本节说明如何从相角计算线路潮流。对每条线路 $ij$,潮流公式为:</p>
<div class="formula-block">$$F_{ij} = \frac{\theta_i - \theta_j}{x_{ij}}$$</div>
<p>其中 $F_{ij}$(线路 $i$-$j$ 上的有功潮流,单位 MW)。将相角代入即可得到各线路潮流值,再与线路限额对比判断哪些线路处于绑定状态(Binding)。</p>
<h4>2.6 验证</h4>
<p>本节验证计算结果的正确性。功率平衡验证:$\sum_i P_i = 0$(所有节点净注入之和为零)。PTDF 验证:$F_k = \sum_i \text{PTDF}_{k,i} \cdot P_i$(为第三章铺垫)。</p>
<h3>第三章:PTDF 功率转移分布因子——灵敏度分析</h3>
<p><strong>本章核心:</strong>PTDF 将"节点注入变化"与"线路潮流变化"直接挂钩,是连接潮流计算与市场优化的桥梁。</p>
<h4>3.1 PTDF 的定义</h4>
<p>本节给出 PTDF 的数学定义:</p>
<div class="formula-block">$$\text{PTDF}_{k,i} = \frac{\Delta F_k}{\Delta P_i}$$</div>
<p>其中 $\text{PTDF}_{k,i}$(线路 $k$ 对节点 $i$ 的功率转移分布因子,无量纲),$\Delta F_k$(线路 $k$ 上的潮流变化量,单位 MW),$\Delta P_i$(节点 $i$ 的净注入变化量,单位 MW)。</p>
<blockquote>PTDF 的物理含义:在节点 $i$ 注入 1MW 有功功率,同时在参考节点抽出 1MW 有功功率时,线路 $k$ 上的潮流变化量。</blockquote>
<h4>3.2 PTDF 的推导</h4>
<p>本节从 DC 潮流方程推导 PTDF。由于 DC 潮流是线性的,线路潮流 $F_k$ 是节点注入 $P_i$ 的线性函数:</p>
<div class="formula-block">$$F_k = \sum_i \text{PTDF}_{k,i} \cdot P_i$$</div>
<p>PTDF 就是这个线性函数的系数,等于偏导数 $\frac{\partial F_k}{\partial P_i}$。从相角方程 $\theta = (B'_{\text{red}})^{-1} P$ 和线路潮流方程 $F_k = (\theta_i - \theta_j)/x_{ij}$ 联立求导,即可得到 PTDF 与逆矩阵元素的关系。</p>
<h4>3.3 参考节点与 PTDF</h4>
<p>本节解释参考节点选择的影响:</p>
<ul>
<li>参考节点的选择是任意的,选不同的参考节点,PTDF 数值会变化</li>
<li>但物理结果不变:线路实际潮流相同,过载判断相同</li>
<li>真实电力系统使用分布式 Slack,不是单一节点承担全部不平衡</li>
</ul>
<h4>3.4 PTDF 矩阵</h4>
<p>以表格形式展示所有线路对所有非参考节点的 PTDF 值。具体数值取决于线路电抗参数,读者可在系统中调整参数观察变化。</p>
<h3>第四章:SCED 安全约束经济调度——市场出清模型</h3>
<p><strong>本章核心:</strong>电力市场通过 SCED 优化模型,在满足功率平衡和线路安全约束的前提下,找到总发电成本最低的发电计划。</p>
<h4>4.1 优化目标</h4>
<p>本节给出 SCED 的目标函数:</p>
<div class="formula-block">$$\min \quad \sum_i c_i \cdot G_i$$</div>
<p>其中 $c_i$(机组 $i$ 的报价,单位元/MWh),$G_i$(机组 $i$ 的发电出力,决策变量,单位 MW)。目标是在满足所有约束的前提下,使总发电成本最小。</p>
<h4>4.2 约束条件</h4>
<p>本节逐一列出并解释 SCED 的三类约束:</p>
<p><strong>约束一:功率平衡约束</strong></p>
<div class="formula-block">$$\sum_i G_i = \sum_i L_i$$</div>
<p>其中 $L_i$(节点 $i$ 的负荷,单位 MW)是已知常数。此约束确保发电总量等于负荷总量。</p>
<p><strong>约束二:线路潮流约束</strong></p>
<div class="formula-block">$$-F_k^{\max} \leq \sum_i \text{PTDF}_{k,i} \cdot (G_i - L_i) \leq F_k^{\max}$$</div>
<p>其中 $F_k^{\max}$(线路 $k$ 的传输限额,单位 MW)。线路潮流等于 PTDF 矩阵乘以净注入向量,必须在限额范围内。</p>
<p><strong>约束三:机组出力约束</strong></p>
<div class="formula-block">$$G_i^{\min} \leq G_i \leq G_i^{\max}$$</div>
<p>其中 $G_i^{\min}$(机组 $i$ 的最小出力,单位 MW),$G_i^{\max}$(机组 $i$ 的最大出力,单位 MW)。</p>
<h4>4.3 为什么叫"安全约束"</h4>
<p>本节对比纯经济调度与安全约束经济调度的区别:</p>
<table>
<tr><th>类型</th><th>是否考虑线路限额</th><th>结果</th></tr>
<tr><td>纯经济调度(ED)</td><td>否</td><td>成本最低,但可能线路过载</td></tr>
<tr><td>安全约束经济调度(SCED)</td><td>是</td><td>成本最低且线路安全</td></tr>
</table>
<h3>第五章:拉格朗日函数与 KKT 条件——求解优化问题</h3>
<p><strong>本章核心:</strong>通过构建拉格朗日函数,将带约束的优化问题转化为无约束问题,再利用 KKT 条件建立方程组求解影子价格。</p>
<h4>5.1 为什么需要拉格朗日函数</h4>
<p>本节解释拉格朗日函数的思想。SCED 是一个有约束的优化问题,无法直接用微积分求极值。拉格朗日函数的思想是:把约束条件"融合"进目标函数,通过引入<strong>拉格朗日乘子</strong>(新变量),将问题转化为无约束优化。</p>
<h4>5.2 拉格朗日函数的构建</h4>
<p>以单线路绑定场景为例,写出完整的拉格朗日函数:</p>
<div class="formula-block">$$\mathcal{L} = \sum_i c_i G_i + \lambda \left( \sum_i L_i - \sum_i G_i \right) + \mu_{AB} \left[ \sum_i \text{PTDF}_{AB,i} \cdot (G_i - L_i) - F_{AB}^{\max} \right]$$</div>
<p>其中 $\mathcal{L}$(拉格朗日函数,单位元),$\lambda$(功率平衡约束的拉格朗日乘子,单位元/MWh),$\mu_{AB}$(线路 AB 限额约束的拉格朗日乘子,单位元/MWh)。</p>
<ul>
<li>第一项 $\sum c_i G_i$:原始目标函数(总发电成本)</li>
<li>第二项 $\lambda(\sum L_i - \sum G_i)$:功率平衡约束的惩罚项,当功率平衡满足时括号内为 0</li>
<li>第三项 $\mu_{AB}[\dots]$:线路 AB 限额约束的惩罚项,当线路潮流等于限额时括号内为 0</li>
</ul>
<h4>5.3 KKT 条件——对决策变量求偏导</h4>
<p>本节对机组出力 $G_1$ 和 $G_2$ 分别求偏导,并令偏导数等于 0:</p>
<p><strong>对 $G_1$ 求偏导:</strong></p>
<div class="formula-block">$$\frac{\partial \mathcal{L}}{\partial G_1} = c_1 - \lambda + \mu_{AB} \cdot \text{PTDF}_{AB,1} = 0$$</div>
<p>$c_1$ 前面是正号(来自目标函数),$-\lambda$ 来自功率平衡约束($G_1$ 的系数为 $-1$),$\mu_{AB} \cdot \text{PTDF}_{AB,1}$ 来自线路约束($G_1$ 的系数为 $\text{PTDF}_{AB,1}$)。</p>
<p><strong>对 $G_2$ 求偏导:</strong></p>
<div class="formula-block">$$\frac{\partial \mathcal{L}}{\partial G_2} = c_2 - \lambda + \mu_{AB} \cdot \text{PTDF}_{AB,2} = 0$$</div>
<h4>5.4 KKT 条件——对乘子求偏导</h4>
<p>本节对拉格朗日乘子求偏导,还原出约束条件本身:</p>
<p><strong>对 $\lambda$ 求偏导:</strong></p>
<div class="formula-block">$$\frac{\partial \mathcal{L}}{\partial \lambda} = \sum_i L_i - \sum_i G_i = 0$$</div>
<p>对乘子求偏导,还原出功率平衡约束。</p>
<p><strong>对 $\mu_{AB}$ 求偏导:</strong></p>
<div class="formula-block">$$\frac{\partial \mathcal{L}}{\partial \mu_{AB}} = \sum_i \text{PTDF}_{AB,i} \cdot (G_i - L_i) - F_{AB}^{\max} = 0$$</div>
<p>线路潮流等于限额,约束处于绑定状态。</p>
<h4>5.5 多线路绑定的情况</h4>
<p>当多条线路同时绑定时,拉格朗日函数包含多个 $\mu_k$ 项,KKT 条件形成更大的线性方程组。求解方法完全相同——联立所有 KKT 方程,解出 $\lambda$ 和所有 $\mu_k$。</p>
<h3>第六章:影子价格——约束条件的稀缺价值</h3>
<p><strong>本章核心:</strong>拉格朗日乘子 $\lambda$ 和 $\mu_k$ 分别反映了功率平衡资源和线路容量资源的稀缺程度,它们就是电力市场中的价格信号。</p>
<h4>6.1 系统边际电价 $\lambda$</h4>
<div class="formula-block">$$\lambda = \frac{d(\text{最优成本})}{d(\text{总负荷})}$$</div>
<p>$\lambda$ 是功率平衡约束的影子价格。当系统总负荷增加 1MW 时,边际机组多发 1MW,系统总发电成本增加的量就是 $\lambda$。在电力市场中,$\lambda$ 被称为<strong>系统边际电价</strong>(System Marginal Price, SMP)。</p>
<h4>6.2 线路影子价格 $\mu_k$</h4>
<div class="formula-block">$$\mu_k = -\frac{d(\text{最优成本})}{d(F_k^{\max})}$$</div>
<p>$\mu_k$ 是线路 $k$ 限额约束的影子价格。当线路 $k$ 的传输限额放宽 1MW 时,系统可以更多地利用便宜机组送电,总发电成本减少的量就是 $\mu_k$。$\mu_k$ 越大,说明该线路的容量越稀缺,阻塞越严重。</p>
<h4>6.3 影子价格的验证方法</h4>
<p>以线路 AB 为例,展示验证过程:</p>
<ol>
<li>记录当前限额 $F_{AB}^{\max}$ 下的最优成本 $C^*$</li>
<li>将限额放宽到 $F_{AB}^{\max} + 1$ MW</li>
<li>重新求解 SCED,得到新的最优成本 $C'$</li>
<li>验证:$C^* - C' = \mu_{AB}$</li>
</ol>
<h4>6.4 影子价格与松弛变量</h4>
<p>硬约束 $F_k \leq F_k^{\max}$ 可能无解,软约束 $F_k + s_k = F_k^{\max}$($s_k \geq 0$)保证有解。当约束绑定时 $s_k=0$,$\mu_k>0$;当约束不绑定时 $s_k>0$,$\mu_k=0$。</p>
<h3>第七章:节点边际电价 LMP——价格的最终形态</h3>
<p><strong>本章核心:</strong>LMP 将系统边际成本与线路阻塞成本叠加,形成反映真实供电成本的节点级价格信号。</p>
<h4>7.1 LMP 的分解公式</h4>
<div class="formula-block">$$\text{LMP}_i = \lambda + \sum_k \mu_k \cdot \text{PTDF}_{k,i}$$</div>
<p>其中 $\text{LMP}_i$(节点 $i$ 的节点边际电价,单位元/MWh),$\lambda$(电能分量,所有节点共享),$\sum_k \mu_k \cdot \text{PTDF}_{k,i}$(阻塞分量,各节点不同)。</p>
<h4>7.2 为什么各节点电价不同</h4>
<p>所有节点共享同一个 $\lambda$,但各节点对各条线路的 PTDF 不同。因此,阻塞分量在不同节点上表现不同,导致 LMP 出现差异。</p>
<h4>7.3 正负 PTDF 对 LMP 的影响</h4>
<table>
<tr><th>PTDF 符号</th><th>机组发电对线路的影响</th><th>阻塞分量方向</th><th>结果</th></tr>
<tr><td>正</td><td>加剧线路使用</td><td>正(叠加)</td><td>LMP 高于 $\lambda$</td></tr>
<tr><td>负</td><td>减少线路使用</td><td>负(抵消)</td><td>LMP 低于 $\lambda$</td></tr>
</table>
<h4>7.4 负电价的产生</h4>
<p>负 LMP 产生的条件:$\lambda$ 较小(边际机组便宜),$\mu_k$ 很大(阻塞严重),$\text{PTDF}_{k,i}$ 为负且绝对值大。此时 $\text{LMP}_i = \lambda + \mu_k \cdot (\text{负数}) < 0$。物理意义:在该节点增加 1MW 负荷,系统总采购成本反而减少。</p>
<h4>7.5 阻塞盈余</h4>
<p>用户按各节点 LMP 支付电费,发电侧按各节点 LMP 获得电费。阻塞盈余 = 用户总支付 − 发电侧总收入。该盈余由市场运营机构或电网企业收取,用于输电权(FTR)的红利结算。</p>
<h3>第八章:中国电力市场实践——理论与现实的距离</h3>
<p><strong>本章核心:</strong>中国山东、广东等现货市场采用 SCED+LMP 的理论框架进行出清计算,但结算环节做了简化,处于"计算用节点、结算用平均"的过渡模式。</p>
<h4>8.1 中国现货市场的"两步走"模式</h4>
<table>
<tr><th>环节</th><th>计算方法</th><th>说明</th></tr>
<tr><td>日前市场出清</td><td>DC 潮流 + SCED + 节点 LMP 计算</td><td>内部计算节点电价</td></tr>
<tr><td>实时市场出清</td><td>DC 潮流 + SCED + 节点 LMP 计算</td><td>5-15 分钟周期</td></tr>
<tr><td>安全校核</td><td>AC 潮流</td><td>验证电压、无功、N-1 安全</td></tr>
<tr><td>发电侧结算</td><td>逐步向节点电价过渡</td><td>部分市场已按节点结算</td></tr>
<tr><td>用户侧结算</td><td>统一结算点电价或分区电价</td><td>大部分用户未直接面对节点电价</td></tr>
</table>
<h4>8.2 与理论模型的差异</h4>
<p>中国没有完全按节点 LMP 结算的原因:用户侧改革滞后,大多数用户通过中长期合同锁定价格;节点电价波动大,市场主体风险承受能力有限;渐进式改革路径,避免市场剧烈波动。</p>
<h4>8.3 山东与广东的特点</h4>
<ul>
<li>山东:发电侧已按节点电价结算,用户侧逐步推进</li>
<li>广东:采用统一结算点电价,通过金融输电权等工具处理阻塞</li>
</ul>
<h3>第九章:全流程总结</h3>
<p><strong>本章核心:</strong>从网络参数到节点电价,全流程共 11 个步骤。</p>
<div class="doc-step"><strong>步骤1:</strong>输入网络拓扑与线路电抗参数</div>
<div class="doc-step"><span class="arrow">↓</span></div>
<div class="doc-step"><strong>步骤2:</strong>构造节点导纳矩阵 $B'$</div>
<div class="doc-step"><span class="arrow">↓</span></div>
<div class="doc-step"><strong>步骤3:</strong>降阶处理(去掉平衡参考节点)</div>
<div class="doc-step"><span class="arrow">↓</span></div>
<div class="doc-step"><strong>步骤4:</strong>求逆矩阵,得到 $(B'_{\text{reduced}})^{-1}$</div>
<div class="doc-step"><span class="arrow">↓</span></div>
<div class="doc-step"><strong>步骤5:</strong>代入净注入功率 $P$,求解节点相角 $\theta$</div>
<div class="doc-step"><span class="arrow">↓</span></div>
<div class="doc-step"><strong>步骤6:</strong>计算线路潮流 $F_{ij} = (\theta_i - \theta_j)/x_{ij}$</div>
<div class="doc-step"><span class="arrow">↓</span></div>
<div class="doc-step"><strong>步骤7:</strong>计算 PTDF 矩阵(偏导数,即线性系数)</div>
<div class="doc-step"><span class="arrow">↓</span></div>
<div class="doc-step"><strong>步骤8:</strong>构建 SCED 优化模型(目标函数 + 功率平衡约束 + 线路限额约束)</div>
<div class="doc-step"><span class="arrow">↓</span></div>
<div class="doc-step"><strong>步骤9:</strong>构建拉格朗日函数,写出 KKT 条件</div>
<div class="doc-step"><span class="arrow">↓</span></div>
<div class="doc-step"><strong>步骤10:</strong>联立求解 $\lambda$(系统边际电价)和 $\mu_k$(线路影子价格)</div>
<div class="doc-step"><span class="arrow">↓</span></div>
<div class="doc-step"><strong>步骤11:</strong>计算各节点 $\text{LMP}_i = \lambda + \sum_k \mu_k \cdot \text{PTDF}_{k,i}$</div>
<h3>附录 A:符号对照表</h3>
<table>
<tr><th>符号</th><th>中文含义</th><th>单位</th><th>首次出现</th></tr>
<tr><td>$c_i$</td><td>机组 $i$ 的报价</td><td>元/MWh</td><td>第四章</td></tr>
<tr><td>$F_{ij}$</td><td>线路 $i$-$j$ 上的有功潮流</td><td>MW</td><td>第二章</td></tr>
<tr><td>$F_k^{\max}$</td><td>线路 $k$ 的传输限额</td><td>MW</td><td>第四章</td></tr>
<tr><td>$G_i$</td><td>机组 $i$ 的发电出力(决策变量)</td><td>MW</td><td>第四章</td></tr>
<tr><td>$G_i^{\max}$</td><td>机组 $i$ 的最大出力</td><td>MW</td><td>第四章</td></tr>
<tr><td>$L_i$</td><td>节点 $i$ 的负荷</td><td>MW</td><td>第一章</td></tr>
<tr><td>$P_i$</td><td>节点 $i$ 的净注入有功功率</td><td>MW</td><td>第一章</td></tr>
<tr><td>$Q_i$</td><td>节点 $i$ 的无功功率</td><td>Mvar</td><td>第一章</td></tr>
<tr><td>$B'$</td><td>节点导纳矩阵</td><td>p.u.</td><td>第二章</td></tr>
<tr><td>$B_{ij}$</td><td>节点 $i$ 与 $j$ 之间的电纳</td><td>p.u.</td><td>第一章</td></tr>
<tr><td>$G_{ij}$</td><td>节点 $i$ 与 $j$ 之间的电导</td><td>p.u.</td><td>第一章</td></tr>
<tr><td>$V_i$</td><td>节点 $i$ 的电压幅值</td><td>p.u.</td><td>第一章</td></tr>
<tr><td>$x_{ij}$</td><td>线路 $i$-$j$ 的电抗</td><td>p.u.</td><td>第一章</td></tr>
<tr><td>$\lambda$</td><td>功率平衡约束的拉格朗日乘子(系统边际电价)</td><td>元/MWh</td><td>第五章</td></tr>
<tr><td>$\mu_k$</td><td>线路 $k$ 限额约束的拉格朗日乘子(线路影子价格)</td><td>元/MWh</td><td>第五章</td></tr>
<tr><td>$\theta_i$</td><td>节点 $i$ 的电压相角</td><td>rad</td><td>第一章</td></tr>
<tr><td>$\mathcal{L}$</td><td>拉格朗日函数</td><td>元</td><td>第五章</td></tr>
<tr><td>$\tilde{S}_i$</td><td>节点 $i$ 的复功率</td><td>MVA</td><td>第一章</td></tr>
<tr><td>$\tilde{V}_i$</td><td>节点 $i$ 的复电压</td><td>p.u.</td><td>第一章</td></tr>
<tr><td>$\text{PTDF}_{k,i}$</td><td>线路 $k$ 对节点 $i$ 的功率转移分布因子</td><td>无量纲</td><td>第三章</td></tr>
</table>
<h3>附录 B:常见问题 FAQ</h3>
<h4>B.1 为什么相角差除以电抗就等于功率?</h4>
<p><strong>核心结论:</strong>这是 DC 潮流三步简化的最终结果。</p>
<p>详细解释:精确的交流功率方程包含电压幅值、相角差、三角函数和导纳。DC 潮流假设电压为 1 p.u.、忽略电阻、将 $\sin\theta \approx \theta$,三步简化后只剩下相角差除以电抗这一项。这不是近似"巧合",而是物理假设的必然推论。</p>
<h4>B.2 复数头顶的弯号是什么意思?</h4>
<p><strong>核心结论:</strong>弯号($\tilde{}$)表示该量为复数,与普通实数区分。</p>
<p>详细解释:交流电压和电流是正弦波,工程师用相量将其表示为复数。复数的实部对应有功分量,虚部对应无功分量。弯号是一种约定记号,提醒读者"这是一个复数,不是标量"。</p>
<h4>B.3 为什么要设计平衡节点?</h4>
<p><strong>核心结论:</strong>数学需要(方程组线性相关)+ 物理兜底(功率实时平衡)。</p>
<p>详细解释:三个节点的功率方程相加为零($\sum P_i = 0$),只有 2 个独立方程。选择一个节点作为参考(Slack),固定其相角为 0,消除自由度。物理上,该节点承担系统中所有微小的功率不平衡,是电网安全运行的"兜底"角色。</p>
<h4>B.4 调度指令真的只下发给平衡节点吗?</h4>
<p><strong>核心结论:</strong>不是,所有机组都参与调度,只是分工不同。</p>
<p>详细解释:SCED 的结果给出所有机组的出力计划,调度机构向所有机组下发调度指令。平衡节点机组的特殊之处在于:它需要响应实时的微小功率偏差(如负荷波动、机组爬坡),而其他机组按计划出力。这不是"只调一个",而是"所有都调,一个兜底"。</p>
<h4>B.5 PTDF 和潮流到底是什么关系?</h4>
<p><strong>核心结论:</strong>PTDF 是潮流对注入的灵敏度,即线性系数。</p>
<p>详细解释:在 DC 潮流的线性假设下,线路潮流 $F_k$ 可以写成所有节点注入 $P_i$ 的线性组合:$F_k = \sum_i \text{PTDF}_{k,i} \cdot P_i$。PTDF 就是这个线性组合的系数矩阵。知道了 PTDF,就可以直接从节点注入计算任意线路的潮流,不需要再解矩阵方程。</p>
<h4>B.6 参考节点必须是 C 吗?</h4>
<p><strong>核心结论:</strong>任意选择,物理结果不变。</p>
<p>详细解释:选不同参考节点,PTDF 数值会变化,但线路实际潮流相同、过载判断相同。这是因为 PTDF 的变化恰好补偿了参考节点变化带来的偏移。本系统选择 C 作为参考,仅是教学方便。</p>
<h4>B.7 为什么所有机组报价都大于 0,LMP 却可能是负的?</h4>
<p><strong>核心结论:</strong>阻塞分量的负值超过了电能分量。</p>
<p>详细解释:$\text{LMP}_i = \lambda + \sum_k \mu_k \cdot \text{PTDF}_{k,i}$。当某节点对某条阻塞线路的 PTDF 为负且绝对值很大时,$\mu_k \cdot \text{PTDF}_{k,i}$ 项可以是一个很大的负数,足以超过正的 $\lambda$,使 LMP 变为负值。物理上,这意味着在该节点增加负荷反而降低了系统总成本。</p>
<h4>B.8 影子价格区分线路和节点吗?</h4>
<p><strong>核心结论:</strong>影子价格对应约束(线路),LMP 是影子价格的组合结果(节点)。</p>
<p>详细解释:$\mu_k$ 对应第 $k$ 条线路的约束,每条线路有一个 $\mu_k$。$\text{LMP}_i$ 是节点 $i$ 的价格,由 $\lambda$ 和所有 $\mu_k$ 按 PTDF 加权组合而成。影子价格是"约束的稀缺价值",LMP 是"节点的供电成本"。</p>
<h4>B.9 真实的电力系统真的用 DC 潮流算 LMP 吗?</h4>
<p><strong>核心结论:</strong>出清用 DC,安全校核用 AC。</p>
<p>详细解释:电力市场出清(SCED)使用 DC 潮流模型,因为计算速度快、结果可解释。出清完成后,系统运行人员使用 AC 潮流进行详细安全校核,检查电压、无功、N-1 安全等 DC 潮流无法覆盖的约束。如果 AC 校核发现问题,会调整出清结果。</p>
<h4>B.10 松弛变量在现货规则中是什么意思?</h4>
<p><strong>核心结论:</strong>松弛变量是软约束机制,保证模型有解。</p>
<p>详细解释:硬约束 $F_k \leq F_k^{\max}$ 在极端工况下可能无可行解。引入松弛变量 $s_k \geq 0$,将约束改为 $F_k + s_k = F_k^{\max}$,并在目标函数中加入惩罚项 $M \cdot s_k$($M$ 为极大正数)。这允许线路略微越限($s_k > 0$),但惩罚极重,求解器会尽量避免。实际市场规则中,松弛变量对应"安全约束越限惩罚"机制。</p>
</div>
</div>
<script>
const NODES = ["a","b","d","e","f"];
const LINE_NAMES = ["ab","ad","bd","ae","be","de","ef"];
const NONREF = ["a","b","d","f"];
const REF = "e";
const SEG_COUNT = 10;
function $(id){ return document.getElementById(id); }
function fvalN(id){ const v = parseFloat($(id).value); return isNaN(v) ? 0 : v; }