File size: 54,966 Bytes
0be7e4d
b2c3381
 
0be7e4d
 
 
 
b2c3381
 
 
 
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
e0ca7be
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2c3381
 
 
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2c3381
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0ca7be
 
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
376535d
 
 
 
 
 
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0ca7be
 
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0ca7be
 
 
 
 
 
 
 
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e0ca7be
0be7e4d
 
e0ca7be
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376535d
 
 
 
 
 
 
e0ca7be
0be7e4d
376535d
 
 
 
 
 
 
 
 
 
 
0be7e4d
376535d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0be7e4d
376535d
0be7e4d
376535d
 
 
e0ca7be
376535d
 
 
0be7e4d
376535d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0be7e4d
 
 
 
 
 
e0ca7be
376535d
0be7e4d
 
 
 
 
 
 
 
 
376535d
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
376535d
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2c3381
 
 
 
 
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5172131
 
 
 
 
 
 
0be7e4d
5172131
 
 
0be7e4d
5172131
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b2c3381
 
 
 
 
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157b77e
0be7e4d
 
 
 
 
157b77e
 
0be7e4d
 
157b77e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0be7e4d
157b77e
 
 
 
 
 
 
 
0be7e4d
157b77e
 
5172131
157b77e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5172131
157b77e
 
 
 
 
5172131
157b77e
 
5172131
157b77e
 
 
5172131
157b77e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
0be7e4d
 
157b77e
0be7e4d
 
 
157b77e
0be7e4d
 
 
 
 
 
157b77e
 
 
0be7e4d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157b77e
 
 
0be7e4d
157b77e
 
0be7e4d
 
157b77e
0be7e4d
157b77e
 
 
 
0be7e4d
157b77e
 
 
 
 
 
0be7e4d
 
b2c3381
 
 
 
 
 
0be7e4d
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
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
import numpy as np
import matplotlib
matplotlib.use('Agg')  # Use non-interactive backend for cloud deployment
import matplotlib.pyplot as plt
from typing import List, Dict, Tuple
import math

# Configure matplotlib for cloud deployment
plt.ioff()  # Turn off interactive mode
plt.rcParams['figure.dpi'] = 80  # Lower DPI for faster rendering
plt.rcParams['savefig.dpi'] = 80

class ContinuousBeam:
    """
    Continuous beam analysis and RC design according to ACI code
    """
    
    def __init__(self):
        self.spans = []
        self.loads = []
        self.supports = []
        self.moments = []
        self.shears = []
        self.fc = 280  # Concrete compressive strength (ksc) - converted from 28 MPa
        self.fy = 4000  # Steel yield strength (ksc) - Thai standard
        self.beam_width = 300  # mm
        self.beam_depth = 500  # mm
        self.cover = 40  # mm
        self.d = self.beam_depth - self.cover  # Effective depth
        
    def add_span(self, length: float, distributed_load: float = 0, point_loads: List[Tuple[float, float]] = None):
        """
        Add a span to the continuous beam
        length: span length in meters
        distributed_load: uniformly distributed load in kN/m
        point_loads: list of (position, load) tuples in (m, kN)
        """
        span = {
            'length': length,
            'distributed_load': distributed_load,
            'point_loads': point_loads or []
        }
        self.spans.append(span)
        
    def create_beam_element_stiffness(self, length, E, I):
        """
        Create local stiffness matrix for a beam element
        [k] = EI/L^3 * [[12, 6L, -12, 6L],
                        [6L, 4L^2, -6L, 2L^2],
                        [-12, -6L, 12, -6L],
                        [6L, 2L^2, -6L, 4L^2]]
        DOFs: [v1, θ1, v2, θ2] where v=deflection, θ=rotation
        """
        L = length
        EI_L3 = E * I / (L**3)
        
        k = EI_L3 * np.array([
            [12,    6*L,   -12,   6*L],
            [6*L,   4*L**2, -6*L,  2*L**2],
            [-12,   -6*L,   12,   -6*L],
            [6*L,   2*L**2, -6*L,  4*L**2]
        ])
        
        return k
    
    def create_distributed_load_vector(self, length, w):
        """
        Create equivalent nodal force vector for distributed load
        For uniformly distributed load w:
        [F] = wL/12 * [6, L, 6, -L]
        """
        L = length
        wL_12 = w * L / 12
        
        f = wL_12 * np.array([6, L, 6, -L])
        return f
    
    def create_point_load_vector(self, length, point_loads):
        """
        Create equivalent nodal force vector for point loads
        """
        L = length
        f = np.zeros(4)
        
        for pos, load in point_loads:
            a = pos  # distance from left node
            b = L - pos  # distance from right node
            
            # Shape functions at load position
            xi = pos / L  # normalized position
            
            # Equivalent nodal forces using shape functions
            N1 = 1 - 3*xi**2 + 2*xi**3
            N2 = L * (xi - 2*xi**2 + xi**3)
            N3 = 3*xi**2 - 2*xi**3
            N4 = L * (-xi**2 + xi**3)
            
            f += load * np.array([N1, N2, N3, N4])
        
        return f
    
    def finite_element_analysis(self):
        """
        Perform finite element analysis of continuous beam
        """
        if len(self.spans) == 0:
            raise ValueError("No spans defined")
        
        # Material properties (assumed values for analysis)
        E = 30000  # MPa (typical for concrete)
        # Calculate moment of inertia from beam dimensions
        b = self.beam_width / 1000  # Convert mm to m
        h = self.beam_depth / 1000  # Convert mm to m
        I = b * h**3 / 12  # m^4
        
        # Create mesh - optimize for cloud deployment
        # Reduce elements per span for better performance on limited resources
        max_elements_per_span = min(8, max(4, int(40 / len(self.spans))))  # Scale down for many spans
        elements_per_span = max_elements_per_span
        total_elements = len(self.spans) * elements_per_span
        total_nodes = total_elements + 1
        
        # Node coordinates
        node_coords = []
        current_x = 0
        
        for span in self.spans:
            span_length = span['length']
            element_length = span_length / elements_per_span
            
            for i in range(elements_per_span):
                node_coords.append(current_x + i * element_length)
            
            current_x += span_length
        
        # Add final node
        node_coords.append(current_x)
        node_coords = np.array(node_coords)
        
        # Global stiffness matrix (2 DOFs per node: deflection and rotation)
        n_dofs = 2 * total_nodes
        K_global = np.zeros((n_dofs, n_dofs))
        F_global = np.zeros(n_dofs)
        
        # Assembly process
        for elem_idx in range(total_elements):
            # Element properties
            span_idx = elem_idx // elements_per_span
            local_elem_idx = elem_idx % elements_per_span
            
            span = self.spans[span_idx]
            element_length = span['length'] / elements_per_span
            
            # Local stiffness matrix
            k_local = self.create_beam_element_stiffness(element_length, E, I)
            
            # Global DOF indices for this element
            node1 = elem_idx
            node2 = elem_idx + 1
            
            dofs = [2*node1, 2*node1+1, 2*node2, 2*node2+1]  # [v1, θ1, v2, θ2]
            
            # Assemble into global matrix
            for i in range(4):
                for j in range(4):
                    K_global[dofs[i], dofs[j]] += k_local[i, j]
            
            # Create load vector for this element
            # Distributed load
            w = span['distributed_load'] * 1000  # Convert kN/m to N/m
            f_dist = self.create_distributed_load_vector(element_length, w)
            
            # Point loads (only if they fall within this element)
            point_loads = span.get('point_loads', [])
            f_point = np.zeros(4)
            
            span_start = sum(self.spans[j]['length'] for j in range(span_idx))
            elem_start = span_start + local_elem_idx * element_length
            elem_end = elem_start + element_length
            
            for pos, load in point_loads:
                global_pos = span_start + pos
                if elem_start <= global_pos <= elem_end:
                    local_pos = global_pos - elem_start
                    f_point += self.create_point_load_vector(element_length, [(local_pos, load * 1000)])
            
            f_total = f_dist + f_point
            
            # Assemble into global force vector
            for i in range(4):
                F_global[dofs[i]] += f_total[i]
        
        # Apply boundary conditions (pin supports at all support locations)
        # Support locations are at the ends of each span
        support_nodes = [0]  # First support
        current_node = 0
        for span in self.spans:
            current_node += elements_per_span
            support_nodes.append(current_node)
        
        # Create arrays for free DOFs (removing constrained deflections)
        constrained_dofs = [2 * node for node in support_nodes]  # Vertical deflections at supports
        free_dofs = [i for i in range(n_dofs) if i not in constrained_dofs]
        
        # Extract free DOF matrices
        K_free = K_global[np.ix_(free_dofs, free_dofs)]
        F_free = F_global[free_dofs]
        
        # Solve for displacements
        try:
            U_free = np.linalg.solve(K_free, F_free)
        except np.linalg.LinAlgError:
            # Fallback to least squares if matrix is singular
            U_free = np.linalg.lstsq(K_free, F_free, rcond=None)[0]
        
        # Reconstruct full displacement vector
        U_global = np.zeros(n_dofs)
        U_global[free_dofs] = U_free
        
        # Store results for post-processing
        self.node_coords = node_coords
        self.displacements = U_global
        self.elements_per_span = elements_per_span
        self.element_properties = {'E': E, 'I': I}
        self.K_global = K_global  # Store for reaction calculation
        
        return node_coords, U_global
    
    def calculate_element_forces(self):
        """
        Calculate internal forces (moment and shear) for each element
        """
        if not hasattr(self, 'displacements'):
            self.finite_element_analysis()
        
        E = self.element_properties['E']
        I = self.element_properties['I']
        
        moments = []
        shears = []
        x_coords = []
        
        # Calculate reactions first for proper shear calculation
        reactions = self.calculate_reactions()
        
        elem_idx = 0
        for span_idx, span in enumerate(self.spans):
            span_length = span['length']
            element_length = span_length / self.elements_per_span
            
            span_start_x = sum(self.spans[j]['length'] for j in range(span_idx))
            
            for local_elem in range(self.elements_per_span):
                # Element nodes
                node1 = elem_idx
                node2 = elem_idx + 1
                
                # Element displacements
                u1 = self.displacements[2*node1]     # deflection at node 1
                theta1 = self.displacements[2*node1+1]  # rotation at node 1
                u2 = self.displacements[2*node2]     # deflection at node 2
                theta2 = self.displacements[2*node2+1]  # rotation at node 2
                
                # Calculate forces at multiple points within element
                # Reduce points for cloud deployment performance
                n_points = 5  # Reduced from 10 to 5
                for i in range(n_points):
                    xi = i / (n_points - 1)  # 0 to 1
                    x_local = xi * element_length
                    x_global = span_start_x + local_elem * element_length + x_local
                    
                    # Shape function derivatives for moment calculation
                    # M = -EI * d²v/dx²
                    d2N1_dx2 = (-6 + 12*xi) / element_length**2
                    d2N2_dx2 = (-4 + 6*xi) / element_length
                    d2N3_dx2 = (6 - 12*xi) / element_length**2
                    d2N4_dx2 = (-2 + 6*xi) / element_length
                    
                    curvature = (d2N1_dx2 * u1 + d2N2_dx2 * theta1 + 
                               d2N3_dx2 * u2 + d2N4_dx2 * theta2)
                    moment = -E * I * curvature / 1000  # Convert to kN-m
                    
                    # Calculate shear using equilibrium method (more reliable)
                    shear = self.calculate_shear_at_position(x_global, reactions)
                    
                    x_coords.append(x_global)
                    moments.append(moment)
                    shears.append(shear)
                
                elem_idx += 1
        
        return np.array(x_coords), np.array(moments), np.array(shears)
    
    def calculate_reactions(self):
        """
        Calculate support reactions from finite element solution
        """
        if not hasattr(self, 'K_global') or not hasattr(self, 'displacements'):
            self.finite_element_analysis()
        
        # Get support node indices
        support_nodes = [0]  # First support
        current_node = 0
        for span in self.spans:
            current_node += self.elements_per_span
            support_nodes.append(current_node)
        
        # Calculate reactions using R = K*U - F for constrained DOFs
        reactions = []
        
        # Build complete force vector including applied loads
        n_dofs = len(self.displacements)
        F_complete = np.zeros(n_dofs)
        
        # Assemble applied load vector (same as in FE analysis)
        elem_idx = 0
        for span_idx, span in enumerate(self.spans):
            element_length = span['length'] / self.elements_per_span
            
            for local_elem_idx in range(self.elements_per_span):
                # Global DOF indices for this element
                node1 = elem_idx
                node2 = elem_idx + 1
                dofs = [2*node1, 2*node1+1, 2*node2, 2*node2+1]
                
                # Create load vector for this element
                w = span['distributed_load'] * 1000  # Convert to N/m
                f_dist = self.create_distributed_load_vector(element_length, w)
                
                # Point loads (only if they fall within this element)
                point_loads = span.get('point_loads', [])
                f_point = np.zeros(4)
                
                span_start = sum(self.spans[j]['length'] for j in range(span_idx))
                elem_start = span_start + local_elem_idx * element_length
                elem_end = elem_start + element_length
                
                for pos, load in point_loads:
                    global_pos = span_start + pos
                    if elem_start <= global_pos <= elem_end:
                        local_pos = global_pos - elem_start
                        f_point += self.create_point_load_vector(element_length, [(local_pos, load * 1000)])
                
                f_total = f_dist + f_point
                
                # Assemble into global force vector
                for i in range(4):
                    F_complete[dofs[i]] += f_total[i]
                
                elem_idx += 1
        
        # Calculate reactions at each support
        for support_node in support_nodes:
            # Vertical DOF for this support
            dof = 2 * support_node
            
            # Reaction = K*U - F at constrained DOF
            # Since displacement is zero at support, reaction = -F_applied + K*U_other
            reaction_force = 0
            
            # Sum contributions from all DOFs
            for j in range(n_dofs):
                reaction_force += self.K_global[dof, j] * self.displacements[j]
            
            # Subtract applied force (if any) at this DOF
            reaction_force -= F_complete[dof]
            
            # Convert to kN and store (positive = upward reaction)
            # Note: FE convention may give negative values for upward reactions
            reactions.append(-reaction_force / 1000)
        
        # Store for debugging
        self.reactions = reactions
        return reactions
    
    def calculate_shear_at_position(self, x_global, reactions):
        """
        Calculate shear force at any position using equilibrium
        """
        shear = 0
        current_pos = 0
        
        # Add reaction at first support
        if len(reactions) > 0:
            shear += reactions[0]
        
        # Subtract loads to the left of current position
        support_idx = 1
        for span_idx, span in enumerate(self.spans):
            span_start = current_pos
            span_end = current_pos + span['length']
            
            if x_global <= span_start:
                break
                
            # Check if we passed a support
            if x_global > span_end and support_idx < len(reactions):
                shear += reactions[support_idx]
                support_idx += 1
            
            # Calculate how much of this span is to the left of current position
            span_length_to_left = min(x_global - span_start, span['length'])
            
            if span_length_to_left > 0:
                # Distributed load effect
                w = span['distributed_load']
                shear -= w * span_length_to_left
                
                # Point load effects
                point_loads = span.get('point_loads', [])
                for pos, load in point_loads:
                    if pos <= span_length_to_left:
                        shear -= load
            
            current_pos += span['length']
        
        return shear
    
    def analyze_moments(self):
        """
        Analyze continuous beam using finite element method
        """
        num_spans = len(self.spans)
        if num_spans == 0:
            raise ValueError("No spans defined")
        
        # Perform finite element analysis
        self.finite_element_analysis()
        
        # Calculate detailed forces along the beam
        x_coords, moments_detailed, shears_detailed = self.calculate_element_forces()
        
        # Extract critical moments and shears for each span (for compatibility with existing code)
        self.moments = []
        self.shears = []
        
        current_pos = 0
        for i, span in enumerate(self.spans):
            span_length = span['length']
            span_start = current_pos
            span_mid = current_pos + span_length / 2
            span_end = current_pos + span_length
            
            # Find indices closest to critical points
            start_idx = np.argmin(np.abs(x_coords - span_start))
            mid_idx = np.argmin(np.abs(x_coords - span_mid))
            end_idx = np.argmin(np.abs(x_coords - span_end))
            
            # Extract moments and shears at critical points
            M_start = moments_detailed[start_idx]
            M_mid = moments_detailed[mid_idx]
            M_end = moments_detailed[end_idx]
            
            V_start = shears_detailed[start_idx]
            V_mid = shears_detailed[mid_idx]
            V_end = shears_detailed[end_idx]
            
            # Store for span (maintaining compatibility with existing design methods)
            self.moments.append([M_start, M_mid, M_end])
            self.shears.append([V_start, V_mid, V_end])
            
            current_pos += span_length
        
        # Store detailed results for plotting
        self.detailed_x = x_coords
        self.detailed_moments = moments_detailed
        self.detailed_shears = shears_detailed
    
    
    def calculate_required_reinforcement(self, moment: float, beam_type: str = "rectangular"):
        """
        Calculate required area of reinforcement according to ACI code
        moment: Design moment in kN-m
        beam_type: Type of beam section
        """
        if moment == 0:
            return 0
            
        # Convert moment to N-mm
        Mu = abs(moment) * 1e6
        
        # Material properties - convert from ksc to MPa
        fc = self.fc / 10.2  # Convert ksc to MPa (1 ksc ≈ 0.098 MPa)
        fy = self.fy / 10.2  # Convert ksc to MPa
        b = self.beam_width  # mm
        d = self.d  # mm
        
        # Strength reduction factor
        phi = 0.9
        
        # Calculate required reinforcement
        # Using simplified rectangular stress block
        beta1 = 0.85 if fc <= 28 else max(0.65, 0.85 - 0.05 * (fc - 28) / 7)
        
        # Calculate Rn
        Rn = Mu / (phi * b * d**2)
        
        # Calculate reinforcement ratio
        # Check for domain error in sqrt
        discriminant = 1 - 2 * Rn / (0.85 * fc)
        if discriminant < 0:
            # Moment exceeds capacity - increase beam size or use compression reinforcement
            raise ValueError(f"Moment exceeds beam capacity. Increase beam size or use compression reinforcement.")
        
        rho = (0.85 * fc / fy) * (1 - math.sqrt(discriminant))
        
        # Minimum reinforcement ratio
        rho_min = max(1.4 / fy, 0.25 * math.sqrt(fc) / fy)
        
        # Maximum reinforcement ratio (75% of balanced ratio)
        rho_b = (0.85 * fc * beta1 * 600) / (fy * (600 + fy))
        rho_max = 0.75 * rho_b
        
        # Check limits
        rho = max(rho, rho_min)
        if rho > rho_max:
            raise ValueError(f"Required reinforcement ratio {rho:.4f} exceeds maximum {rho_max:.4f}")
        
        # Calculate required area
        As_required = rho * b * d
        
        return As_required
    
    def calculate_shear_reinforcement(self, shear: float):
        """
        Calculate shear reinforcement (stirrups) according to ACI code
        shear: Design shear force in kN
        """
        if shear == 0:
            return {"stirrup_spacing": "No stirrups required", "Av": 0}
            
        # Convert shear to N
        Vu = abs(shear) * 1000
        
        # Material properties - convert from ksc to MPa
        fc = self.fc / 10.2  # Convert ksc to MPa
        fy = self.fy / 10.2  # Convert ksc to MPa (for stirrups)
        b = self.beam_width  # mm
        d = self.d  # mm
        
        # Strength reduction factor for shear
        phi_v = 0.75
        
        # Concrete shear capacity
        Vc = 0.17 * math.sqrt(fc) * b * d  # N
        
        # Check if shear reinforcement is required
        if Vu <= phi_v * Vc / 2:
            return {"stirrup_spacing": "No stirrups required", "Av": 0}
        
        # Calculate required shear reinforcement
        Vs = Vu / phi_v - Vc  # Required steel shear capacity
        
        # Maximum shear that can be carried by steel
        Vs_max = 0.66 * math.sqrt(fc) * b * d
        
        if Vs > Vs_max:
            raise ValueError("Shear exceeds maximum capacity - increase beam size")
        
        # Calculate required stirrup area
        # Use RB9 or RB6 stirrups based on shear demand
        if Vs > 150000:  # High shear - use RB9
            stirrup_dia = 9
            Av = 2 * math.pi * (9/2)**2  # 2-leg RB9 stirrups = 2 × 63.6 = 127 mm²
            stirrup_designation = "RB9"
        else:  # Lower shear - use RB6
            stirrup_dia = 6
            Av = 2 * math.pi * (6/2)**2  # 2-leg RB6 stirrups = 2 × 28.3 = 57 mm²
            stirrup_designation = "RB6"
        
        # Calculate required spacing
        s_required = Av * fy * d / Vs  # mm
        
        # Maximum spacing limits
        s_max = min(d / 2, 600)  # mm
        
        # Minimum stirrup requirements
        if Vu > phi_v * Vc:
            Av_min = 0.35 * b * s_required / fy
            s_max_min = min(d / 4, 300)  # More restrictive for high shear
            s_required = min(s_required, s_max_min)
        
        s_required = min(s_required, s_max)
        s_required = max(s_required, 50)  # Minimum practical spacing
        
        return {
            "stirrup_spacing": f"{stirrup_designation} @ {s_required:.0f} mm c/c",
            "Av": Av,
            "Vs": Vs / 1000,  # Convert back to kN
            "Vc": Vc / 1000,   # Convert back to kN
            "stirrup_type": stirrup_designation
        }
    
    def design_beam(self):
        """
        Complete beam design including flexural and shear design
        """
        if not self.moments:
            self.analyze_moments()
        
        design_results = []
        
        for i, (moments, shears) in enumerate(zip(self.moments, self.shears)):
            span_design = {
                'span': i + 1,
                'length': self.spans[i]['length'],
                'moments': moments,
                'shears': shears,
                'reinforcement': [],
                'stirrups': []
            }
            
            # Design for each critical section
            moment_locations = ['Left Support', 'Mid-span', 'Right Support']
            
            for j, (moment, shear) in enumerate(zip(moments, shears)):
                # Flexural design
                if moment != 0:
                    As_required = self.calculate_required_reinforcement(moment)
                    
                    # Select reinforcement bars - Thai DB bars with spacing check
                    bar_data = {
                        12: {'area': 113, 'diameter': 12},   # DB12
                        16: {'area': 201, 'diameter': 16},   # DB16
                        20: {'area': 314, 'diameter': 20},   # DB20
                        24: {'area': 452, 'diameter': 24},   # DB24
                        32: {'area': 804, 'diameter': 32}    # DB32
                    }
                    
                    # Calculate minimum spacing requirements
                    cover = self.cover
                    stirrup_dia = 9  # Assume RB9 stirrups
                    
                    # Try different bar sizes with spacing check
                    selected = False
                    for bar_size in sorted(bar_data.keys()):
                        bar_info = bar_data[bar_size]
                        bar_area = bar_info['area']
                        bar_diameter = bar_info['diameter']
                        
                        num_bars = math.ceil(As_required / bar_area)
                        
                        # Check practical limits
                        if num_bars > 8:  # Too many bars
                            continue
                            
                        if num_bars < 2:  # Minimum 2 bars
                            num_bars = 2
                        
                        # Calculate required spacing
                        # Available width = beam_width - 2×cover - 2×stirrup_dia
                        available_width = self.beam_width - 2*cover - 2*stirrup_dia
                        
                        # Required spacing = (available_width - num_bars×bar_diameter) / (num_bars-1)
                        if num_bars > 1:
                            required_spacing = (available_width - num_bars * bar_diameter) / (num_bars - 1)
                        else:
                            required_spacing = available_width  # Single bar case
                        
                        # Minimum spacing = max(25mm, bar_diameter, aggregate_size)
                        # Use conservative 25mm minimum
                        min_spacing = max(25, bar_diameter)
                        
                        # Check if spacing is adequate
                        if required_spacing >= min_spacing:
                            As_provided = num_bars * bar_area
                            selected = True
                            break
                    
                    if not selected:
                        # If no bar size works, use largest bars and warn
                        bar_size = 32
                        bar_area = bar_data[32]['area']
                        bar_diameter = bar_data[32]['diameter']
                        num_bars = max(2, math.ceil(As_required / bar_area))
                        As_provided = num_bars * bar_area
                        
                        # Calculate actual spacing for warning
                        available_width = self.beam_width - 2*cover - 2*stirrup_dia
                        if num_bars > 1:
                            actual_spacing = (available_width - num_bars * bar_diameter) / (num_bars - 1)
                        else:
                            actual_spacing = available_width
                        
                        if actual_spacing < 25:
                            print(f"Warning: Tight bar spacing ({actual_spacing:.1f}mm) at {moment_locations[j]}. Consider increasing beam width.")
                    
                    # Calculate final spacing for display
                    available_width = self.beam_width - 2*cover - 2*stirrup_dia
                    if num_bars > 1:
                        final_spacing = (available_width - num_bars * bar_diameter) / (num_bars - 1)
                    else:
                        final_spacing = available_width
                    
                    reinforcement = {
                        'location': moment_locations[j],
                        'moment': moment,
                        'As_required': As_required,
                        'As_provided': As_provided,
                        'bars': f"{num_bars}-DB{bar_size}",
                        'spacing': f"{final_spacing:.0f}mm",
                        'ratio': As_provided / (self.beam_width * self.d) * 100
                    }
                else:
                    reinforcement = {
                        'location': moment_locations[j],
                        'moment': 0,
                        'As_required': 0,
                        'As_provided': 0,
                        'bars': "No reinforcement",
                        'spacing': "N/A",
                        'ratio': 0
                    }
                
                span_design['reinforcement'].append(reinforcement)
                
                # Shear design
                stirrup_design = self.calculate_shear_reinforcement(shear)
                stirrup_design['location'] = moment_locations[j]
                stirrup_design['shear'] = shear
                span_design['stirrups'].append(stirrup_design)
            
            design_results.append(span_design)
        
        return design_results
    
    def generate_report(self, design_results):
        """
        Generate design report
        """
        report = []
        report.append("="*60)
        report.append("CONTINUOUS BEAM RC DESIGN REPORT")
        report.append("According to ACI Code")
        report.append("="*60)
        report.append(f"Beam dimensions: {self.beam_width}mm × {self.beam_depth}mm")
        report.append(f"Concrete strength (f'c): {self.fc} MPa")
        report.append(f"Steel strength (fy): {self.fy} MPa")
        report.append(f"Effective depth (d): {self.d} mm")
        report.append("")
        
        for span_data in design_results:
            report.append(f"SPAN {span_data['span']} - Length: {span_data['length']} m")
            report.append("-" * 40)
            
            # Moments and reinforcement
            report.append("FLEXURAL DESIGN:")
            for reinf in span_data['reinforcement']:
                if reinf['moment'] != 0:
                    report.append(f"  {reinf['location']}:")
                    report.append(f"    Moment: {reinf['moment']:.2f} kN-m")
                    report.append(f"    As required: {reinf['As_required']:.0f} mm²")
                    report.append(f"    As provided: {reinf['As_provided']:.0f} mm²")
                    report.append(f"    Reinforcement: {reinf['bars']}")
                    report.append(f"    Bar spacing: {reinf['spacing']}")
                    report.append(f"    Reinforcement ratio: {reinf['ratio']:.2f}%")
                    report.append("")
            
            # Shear and stirrups
            report.append("SHEAR DESIGN:")
            for stirrup in span_data['stirrups']:
                if stirrup['shear'] != 0:
                    report.append(f"  {stirrup['location']}:")
                    report.append(f"    Shear: {stirrup['shear']:.2f} kN")
                    if 'Vs' in stirrup:
                        report.append(f"    Vc: {stirrup['Vc']:.2f} kN")
                        report.append(f"    Vs: {stirrup['Vs']:.2f} kN")
                    report.append(f"    Stirrup spacing: {stirrup['stirrup_spacing']}")
                    report.append("")
            
            report.append("")
        
        return "\n".join(report)
    
    def plot_bmd_sfd(self, design_results=None):
        """
        Generate BMD and SFD plots
        """
        if design_results is None:
            design_results = self.design_beam()
        
        fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 8))
        
        # Calculate total beam length and positions
        total_length = 0
        span_positions = [0]
        
        for span in self.spans:
            total_length += span['length']
            span_positions.append(total_length)
        
        # Use detailed FE results if available, otherwise fall back to approximate method
        if hasattr(self, 'detailed_x') and hasattr(self, 'detailed_moments'):
            # Use finite element results
            x_coords = self.detailed_x
            moments_detailed = self.detailed_moments
            shears_detailed = self.detailed_shears
        else:
            # Fallback to approximate method for backwards compatibility
            x_coords = []
            moments_detailed = []
            shears_detailed = []
            
            for i, span_data in enumerate(design_results):
                span_length = span_data['length']
                start_pos = span_positions[i]
                
                # Create x coordinates for this span
                x_span = np.linspace(start_pos, start_pos + span_length, 100)
                
                # Get moments and shears for this span
                moments = span_data['moments']  # [left, mid, right]
                shears = span_data['shears']
                
                # Simple interpolation between critical points
                moment_curve = np.interp(x_span, 
                                       [start_pos, start_pos + span_length/2, start_pos + span_length],
                                       moments)
                shear_curve = np.interp(x_span,
                                      [start_pos, start_pos + span_length/2, start_pos + span_length],
                                      shears)
                
                x_coords.extend(x_span)
                moments_detailed.extend(moment_curve)
                shears_detailed.extend(shear_curve)
        
        # Plot BMD
        ax1.plot(x_coords, moments_detailed, 'b-', linewidth=2, label='Bending Moment')
        ax1.fill_between(x_coords, moments_detailed, alpha=0.3, color='blue')
        ax1.axhline(y=0, color='k', linestyle='-', alpha=0.3)
        ax1.set_ylabel('Bending Moment (kN-m)', fontsize=12)
        ax1.set_title('Bending Moment Diagram (BMD)', fontsize=14, fontweight='bold')
        ax1.grid(True, alpha=0.3)
        ax1.legend()
        
        # Add support symbols
        for pos in span_positions:
            ax1.axvline(x=pos, color='red', linestyle='--', alpha=0.7)
            ax1.plot(pos, 0, 'rs', markersize=8, label='Support' if pos == span_positions[0] else "")
        
        # Plot SFD  
        ax2.plot(x_coords, shears_detailed, 'r-', linewidth=2, label='Shear Force')
        ax2.fill_between(x_coords, shears_detailed, alpha=0.3, color='red')
        ax2.axhline(y=0, color='k', linestyle='-', alpha=0.3)
        ax2.set_ylabel('Shear Force (kN)', fontsize=12)
        ax2.set_xlabel('Distance along beam (m)', fontsize=12)
        ax2.set_title('Shear Force Diagram (SFD)', fontsize=14, fontweight='bold')
        ax2.grid(True, alpha=0.3)
        ax2.legend()
        
        # Add support symbols
        for pos in span_positions:
            ax2.axvline(x=pos, color='red', linestyle='--', alpha=0.7)
            ax2.plot(pos, 0, 'rs', markersize=8, label='Support' if pos == span_positions[0] else "")
        
        plt.tight_layout()
        
        # Close any other open figures to free memory
        for i in plt.get_fignums():
            if i != fig.number:
                plt.close(i)
        
        return fig
    
    def plot_reinforcement_layout(self, design_results=None):
        """
        Generate reinforcement layout diagram
        """
        if design_results is None:
            design_results = self.design_beam()
        
        fig, ax = plt.subplots(1, 1, figsize=(14, 8))
        
        # Calculate positions
        total_length = sum(span['length'] for span in self.spans)
        span_positions = [0]
        current_pos = 0
        
        for span in self.spans:
            current_pos += span['length']
            span_positions.append(current_pos)
        
        # Draw beam outline
        beam_height = 0.5  # Normalized height for drawing
        ax.add_patch(plt.Rectangle((0, -beam_height/2), total_length, beam_height, 
                                  fill=False, edgecolor='black', linewidth=2))
        
        # Add beam dimensions text
        ax.text(total_length/2, beam_height/2 + 0.1, 
               f'{self.beam_width}mm × {self.beam_depth}mm', 
               ha='center', va='bottom', fontsize=10, fontweight='bold')
        
        # Draw reinforcement for each span
        colors = ['blue', 'green', 'orange', 'purple', 'brown']
        
        for i, span_data in enumerate(design_results):
            span_start = span_positions[i]
            span_end = span_positions[i + 1]
            span_center = (span_start + span_end) / 2
            color = colors[i % len(colors)]
            
            # Process reinforcement
            for j, reinf in enumerate(span_data['reinforcement']):
                if reinf['As_provided'] > 0:
                    location = reinf['location']
                    bars = reinf['bars']
                    
                    if location == 'Left Support':
                        x_pos = span_start
                        y_pos = beam_height/3  # Top reinforcement
                        marker = '^'
                        label_pos = 'top'
                    elif location == 'Mid-span':
                        x_pos = span_center
                        y_pos = -beam_height/3  # Bottom reinforcement
                        marker = 'v'
                        label_pos = 'bottom'
                    else:  # Right Support
                        x_pos = span_end
                        y_pos = beam_height/3  # Top reinforcement
                        marker = '^'
                        label_pos = 'top'
                    
                    # Draw reinforcement symbol
                    ax.scatter(x_pos, y_pos, s=100, c=color, marker=marker, 
                             edgecolor='black', linewidth=1, zorder=5)
                    
                    # Get spacing information for this reinforcement
                    spacing_info = ""
                    if 'spacing' in reinf and reinf['spacing'] != "N/A":
                        spacing_info = f"\nSpacing: {reinf['spacing']}"
                    
                    # Add reinforcement label with spacing
                    label_text = bars + spacing_info
                    if label_pos == 'top':
                        ax.text(x_pos, y_pos + 0.15, label_text, ha='center', va='bottom', 
                               fontsize=9, fontweight='bold', rotation=0,
                               bbox=dict(boxstyle="round,pad=0.2", facecolor="lightblue", alpha=0.8))
                    else:
                        ax.text(x_pos, y_pos - 0.15, label_text, ha='center', va='top', 
                               fontsize=9, fontweight='bold', rotation=0,
                               bbox=dict(boxstyle="round,pad=0.2", facecolor="lightblue", alpha=0.8))
                    
                    # Draw dimension lines for bar spacing if multiple bars
                    if 'num_bars' in reinf and reinf['num_bars'] > 1 and 'spacing' in reinf and reinf['spacing'] != "N/A":
                        bar_count = reinf['num_bars']
                        # Extract numerical value from spacing string (e.g., "150mm" -> 150)
                        try:
                            spacing_mm = float(reinf['spacing'].replace('mm', ''))
                            spacing = spacing_mm / 1000  # Convert mm to m for plotting
                        except:
                            continue  # Skip if spacing format is unexpected
                        
                        # Calculate bar positions along the beam width (shown as small offset from main position)
                        if location == 'Mid-span':  # Bottom bars
                            # Show individual bar positions
                            total_bar_width = (bar_count - 1) * spacing / 20  # Scale for visualization
                            start_offset = -total_bar_width / 2
                            
                            for bar_idx in range(bar_count):
                                bar_x = x_pos + start_offset + (bar_idx * total_bar_width / (bar_count - 1) if bar_count > 1 else 0)
                                ax.scatter(bar_x, y_pos, s=30, c='darkblue', marker='o', 
                                         edgecolor='black', linewidth=0.5, zorder=6, alpha=0.7)
                            
                            # Add spacing dimension line below
                            if bar_count > 1:
                                dim_y = y_pos - 0.25
                                ax.annotate('', xy=(x_pos + start_offset, dim_y), 
                                           xytext=(x_pos + start_offset + total_bar_width, dim_y),
                                           arrowprops=dict(arrowstyle='<->', color='red', lw=1))
                                ax.text(x_pos, dim_y - 0.05, f'{reinf["spacing"]} c/c', 
                                       ha='center', va='top', fontsize=7, color='red', fontweight='bold')
        
        # Draw supports
        for i, pos in enumerate(span_positions):
            # Support triangle
            triangle_height = 0.2
            triangle_width = 0.1
            
            triangle = plt.Polygon([
                [pos - triangle_width/2, -beam_height/2],
                [pos + triangle_width/2, -beam_height/2],
                [pos, -beam_height/2 - triangle_height]
            ], fill=True, facecolor='red', edgecolor='black')
            
            ax.add_patch(triangle)
            
            # Support label
            ax.text(pos, -beam_height/2 - triangle_height - 0.1, 
                   f'Support {i+1}', ha='center', va='top', fontsize=8)
        
        # Add span labels and loads
        for i, span in enumerate(self.spans):
            span_start = span_positions[i]
            span_end = span_positions[i + 1]
            span_center = (span_start + span_end) / 2
            
            # Create span label text
            label_text = f'Span {i+1}\nL = {span["length"]}m\nw = {span["distributed_load"]}kN/m'
            
            # Add point loads to label if any
            if span.get('point_loads'):
                point_load_text = '\nPoint Loads:'
                for pos, load in span['point_loads']:
                    point_load_text += f'\n{load}kN @ {pos}m'
                label_text += point_load_text
            
            # Span label
            ax.text(span_center, -beam_height/2 - 0.4, label_text, 
                   ha='center', va='top', fontsize=9,
                   bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.7))
            
            # Distributed load arrows
            if span["distributed_load"] > 0:
                num_arrows = 5
                for j in range(num_arrows):
                    x_arrow = span_start + (span_end - span_start) * j / (num_arrows - 1)
                    ax.arrow(x_arrow, beam_height/2 + 0.3, 0, -0.2, 
                            head_width=0.05, head_length=0.05, fc='red', ec='red')
            
            # Point load arrows
            if span.get('point_loads'):
                for pos, load in span['point_loads']:
                    x_point = span_start + pos
                    # Larger arrow for point loads
                    ax.arrow(x_point, beam_height/2 + 0.5, 0, -0.4, 
                            head_width=0.08, head_length=0.08, fc='blue', ec='blue', linewidth=2)
                    # Point load label
                    ax.text(x_point, beam_height/2 + 0.6, f'{load}kN', 
                           ha='center', va='bottom', fontsize=8, fontweight='bold', color='blue')
        
        # Formatting
        ax.set_xlim(-0.5, total_length + 0.5)
        ax.set_ylim(-1.2, 1.0)
        ax.set_xlabel('Distance along beam (m)', fontsize=12)
        ax.set_title('Reinforcement Layout', fontsize=14, fontweight='bold')
        ax.grid(True, alpha=0.3)
        ax.set_aspect('equal')
        
        # Legend
        legend_elements = [
            plt.scatter([], [], s=100, c='blue', marker='^', edgecolor='black', 
                       label='Top Reinforcement (Negative Moment)'),
            plt.scatter([], [], s=100, c='blue', marker='v', edgecolor='black', 
                       label='Bottom Reinforcement (Positive Moment)')
        ]
        ax.legend(handles=legend_elements, loc='upper right')
        
        plt.tight_layout()
        
        # Close any other open figures to free memory
        for i in plt.get_fignums():
            if i != fig.number:
                plt.close(i)
        
        return fig
    
    def plot_stirrup_layout(self, design_results=None):
        """
        Generate shear stirrup layout diagram
        """
        if design_results is None:
            design_results = self.design_beam()
        
        fig, ax = plt.subplots(1, 1, figsize=(14, 6))
        
        # Calculate positions
        total_length = sum(span['length'] for span in self.spans)
        span_positions = [0]
        current_pos = 0
        
        for span in self.spans:
            current_pos += span['length']
            span_positions.append(current_pos)
        
        # Draw beam outline (side view)
        beam_height = 0.5
        ax.add_patch(plt.Rectangle((0, 0), total_length, beam_height, 
                                  fill=False, edgecolor='black', linewidth=2))
        
        # Draw detailed stirrup layout for each span
        for i, span_data in enumerate(design_results):
            span_start = span_positions[i]
            span_end = span_positions[i + 1]
            span_length = span_end - span_start
            
            # Get stirrup information with locations
            stirrup_regions = []
            for stirrup in span_data['stirrups']:
                if 'No stirrups' not in stirrup['stirrup_spacing']:
                    # Extract stirrup type and spacing
                    stirrup_parts = stirrup['stirrup_spacing'].split(' @ ')
                    if len(stirrup_parts) == 2:
                        stirrup_type = stirrup_parts[0]  # e.g., "RB9" or "RB6"
                        spacing_str = stirrup_parts[1].replace(' mm c/c', '')
                        try:
                            spacing_mm = float(spacing_str)
                            spacing_m = spacing_mm / 1000
                            
                            stirrup_regions.append({
                                'location': stirrup['location'],
                                'type': stirrup_type,
                                'spacing_mm': spacing_mm,
                                'spacing_m': spacing_m,
                                'shear': stirrup['shear']
                            })
                        except:
                            pass
            
            if stirrup_regions:
                # Create detailed stirrup pattern for the span
                # Divide span into regions based on stirrup requirements
                regions = {
                    'Left Support': {'start': span_start, 'end': span_start + span_length * 0.25},
                    'Mid-span': {'start': span_start + span_length * 0.25, 'end': span_start + span_length * 0.75},
                    'Right Support': {'start': span_start + span_length * 0.75, 'end': span_end}
                }
                
                stirrup_positions = []
                stirrup_labels = []
                
                for stirrup_region in stirrup_regions:
                    location = stirrup_region['location']
                    if location in regions:
                        region_start = regions[location]['start']
                        region_end = regions[location]['end']
                        region_length = region_end - region_start
                        spacing = stirrup_region['spacing_m']
                        
                        # Calculate stirrup positions in this region
                        num_stirrups = max(2, int(region_length / spacing) + 1)
                        actual_spacing = region_length / (num_stirrups - 1) if num_stirrups > 1 else region_length
                        
                        for j in range(num_stirrups):
                            x_pos = region_start + j * actual_spacing
                            if x_pos <= region_end:
                                stirrup_positions.append(x_pos)
                                stirrup_labels.append({
                                    'x': x_pos,
                                    'type': stirrup_region['type'],
                                    'spacing': stirrup_region['spacing_mm'],
                                    'location': location
                                })
                
                # Draw all stirrups
                colors = {'RB6': 'green', 'RB9': 'darkgreen'}
                for pos in stirrup_positions:
                    # Draw stirrup as detailed U-shape
                    stirrup_width = 0.02
                    
                    # Main vertical lines
                    ax.plot([pos, pos], [beam_height*0.05, beam_height*0.95], 'g-', linewidth=3, alpha=0.8)
                    
                    # Horizontal top and bottom connections
                    ax.plot([pos-stirrup_width, pos+stirrup_width], [beam_height*0.05, beam_height*0.05], 'g-', linewidth=2, alpha=0.8)
                    ax.plot([pos-stirrup_width, pos+stirrup_width], [beam_height*0.95, beam_height*0.95], 'g-', linewidth=2, alpha=0.8)
                    
                    # Side connections
                    ax.plot([pos-stirrup_width, pos-stirrup_width], [beam_height*0.05, beam_height*0.95], 'g-', linewidth=2, alpha=0.8)
                    ax.plot([pos+stirrup_width, pos+stirrup_width], [beam_height*0.05, beam_height*0.95], 'g-', linewidth=2, alpha=0.8)
                
                # Add spacing dimensions between stirrups
                if len(stirrup_positions) >= 2:
                    # Group consecutive stirrups and show spacing
                    prev_pos = stirrup_positions[0]
                    for k in range(1, min(4, len(stirrup_positions))):  # Show first few spacings
                        curr_pos = stirrup_positions[k]
                        spacing_actual = (curr_pos - prev_pos) * 1000  # Convert to mm
                        
                        # Dimension line above beam
                        dim_y = beam_height + 0.15 + (k-1) * 0.08
                        ax.annotate('', xy=(prev_pos, dim_y), xytext=(curr_pos, dim_y),
                                   arrowprops=dict(arrowstyle='<->', color='red', lw=1.5))
                        
                        # Spacing text
                        ax.text((prev_pos + curr_pos) / 2, dim_y + 0.02, f'{spacing_actual:.0f}mm', 
                               ha='center', va='bottom', fontsize=7, color='red', fontweight='bold',
                               bbox=dict(boxstyle="round,pad=0.1", facecolor="white", alpha=0.9))
                        
                        # Vertical dimension lines
                        ax.plot([prev_pos, prev_pos], [beam_height, dim_y - 0.01], 'r--', linewidth=1, alpha=0.5)
                        ax.plot([curr_pos, curr_pos], [beam_height, dim_y - 0.01], 'r--', linewidth=1, alpha=0.5)
                        
                        prev_pos = curr_pos
                
                # Add stirrup type and spacing summary
                mid_span = (span_start + span_end) / 2
                
                # Create summary text for stirrup types used
                stirrup_summary = []
                for region in stirrup_regions:
                    stirrup_summary.append(f"{region['type']} @ {region['spacing_mm']:.0f}mm ({region['location']})")
                
                summary_text = "\n".join(stirrup_summary)
                ax.text(mid_span, beam_height + 0.4, f'Span {i+1} Stirrups:\n{summary_text}', 
                       ha='center', va='bottom', fontsize=8,
                       bbox=dict(boxstyle="round,pad=0.3", facecolor="lightgreen", alpha=0.7))
        
        # Draw supports
        for i, pos in enumerate(span_positions):
            # Support line
            ax.plot([pos, pos], [-0.1, beam_height + 0.05], 'r--', linewidth=2, alpha=0.7)
            
            # Support symbol (triangle)
            triangle = plt.Polygon([
                [pos - 0.05, -0.1],
                [pos + 0.05, -0.1],
                [pos, -0.2]
            ], fill=True, facecolor='red', edgecolor='black')
            ax.add_patch(triangle)
            
            # Support label
            ax.text(pos, -0.25, f'Support {i+1}', ha='center', va='top', fontsize=8, fontweight='bold')
        
        # Add dimension lines and labels
        for i, span in enumerate(self.spans):
            span_start = span_positions[i]
            span_end = span_positions[i + 1]
            span_center = (span_start + span_end) / 2
            
            # Dimension line
            ax.annotate('', xy=(span_start, -0.3), xytext=(span_end, -0.3),
                       arrowprops=dict(arrowstyle='<->', color='black', lw=1))
            
            # Span length label
            ax.text(span_center, -0.35, f'{span["length"]}m', 
                   ha='center', va='top', fontsize=10)
        
        # Formatting with more space for detailed annotations
        ax.set_xlim(-0.3, total_length + 0.3)
        ax.set_ylim(-0.6, beam_height + 0.8)
        ax.set_xlabel('Distance along beam (m)', fontsize=12)
        ax.set_ylabel('Beam Cross-Section', fontsize=12)
        ax.set_title('Detailed Shear Stirrup Layout with Spacing Dimensions', fontsize=14, fontweight='bold')
        ax.grid(True, alpha=0.3)
        
        # Add comprehensive legend
        legend_elements = [
            plt.Line2D([0], [0], color='green', linewidth=3, alpha=0.8, label='Stirrups (U-shaped)'),
            plt.Line2D([0], [0], color='red', linestyle='-', linewidth=1.5, label='Spacing Dimensions'),
            plt.Line2D([0], [0], color='red', linestyle='--', linewidth=2, alpha=0.7, label='Supports'),
            plt.Rectangle((0,0),1,1, facecolor='lightgreen', alpha=0.7, label='Stirrup Details')
        ]
        ax.legend(handles=legend_elements, loc='upper right', fontsize=10)
        
        # Add beam dimensions annotation
        ax.text(total_length/2, -0.45, f'Beam: {self.beam_width}mm × {self.beam_depth}mm', 
               ha='center', va='center', fontsize=10, fontweight='bold',
               bbox=dict(boxstyle="round,pad=0.3", facecolor="lightyellow", alpha=0.8))
        
        plt.tight_layout()
        
        # Close any other open figures to free memory
        for i in plt.get_fignums():
            if i != fig.number:
                plt.close(i)
        
        return fig