@@ -779,6 +779,312 @@ if @isdefined(ModelingToolkit)
779779 ]
780780 @test isapprox (fr, reference_fr)
781781 end
782+
783+ @testset " isolate_subsystem" begin
784+ @testset " basic plant isolation" begin
785+ @named P = FirstOrder (k = 1 , T = 1 )
786+ @named C = Blocks. Gain (k = - 1 )
787+
788+ eqs = [connect (C. output, :u , P. input), connect (P. output, :y , C. input)]
789+ sys = System (eqs, t, systems = [P, C], name = :cl )
790+
791+ isolated, input_vars, output_vars = isolate_subsystem (sys, :u , :y )
792+
793+ @test length (get_systems (isolated)) == 1
794+ @test nameof (only (get_systems (isolated))) == :P
795+ @test isempty (get_eqs (isolated))
796+ @test isequal (only (input_vars), P. input. u)
797+ @test isequal (only (output_vars), P. output. u)
798+ end
799+
800+ @testset " external components are removed" begin
801+ @named P = FirstOrder (k = 1 , T = 1 )
802+ @named C = Blocks. Gain (k = 1 )
803+ @named add = Blocks. Add (k2 = - 1 )
804+ @named ref = Step ()
805+
806+ eqs = [
807+ connect (ref. output, add. input1)
808+ connect (P. output, :y , add. input2)
809+ connect (add. output, C. input)
810+ connect (C. output, :u , P. input)
811+ ]
812+ sys = System (eqs, t, systems = [P, C, add, ref], name = :cl )
813+
814+ isolated, input_vars, output_vars = isolate_subsystem (sys, :u , :y )
815+
816+ @test Set (nameof .(get_systems (isolated))) == Set ([:P ])
817+ @test isempty (get_eqs (isolated))
818+ @test isequal (only (input_vars), P. input. u)
819+ @test isequal (only (output_vars), P. output. u)
820+ end
821+
822+ @testset " internal connections are preserved" begin
823+ @named P1 = FirstOrder (k = 1 , T = 1 )
824+ @named P2 = FirstOrder (k = 1 , T = 1 )
825+ @named C = Blocks. Gain (k = - 1 )
826+
827+ eqs = [
828+ connect (C. output, :u , P1. input)
829+ connect (P1. output, P2. input)
830+ connect (P2. output, :y , C. input)
831+ ]
832+ sys = System (eqs, t, systems = [P1, P2, C], name = :cl )
833+
834+ isolated, input_vars, output_vars = isolate_subsystem (sys, :u , :y )
835+
836+ @test Set (nameof .(get_systems (isolated))) == Set ([:P1 , :P2 ])
837+ @test length (get_eqs (isolated)) == 1
838+ @test value (only (get_eqs (isolated)). rhs) isa Connection
839+ @test isequal (only (input_vars), P1. input. u)
840+ @test isequal (only (output_vars), P2. output. u)
841+ end
842+
843+ @testset " reachability finds intermediate inside components" begin
844+ @named P1 = FirstOrder (k = 1 , T = 1 )
845+ @named P_mid = FirstOrder (k = 1 , T = 1 )
846+ @named P2 = FirstOrder (k = 1 , T = 1 )
847+ @named C = Blocks. Gain (k = - 1 )
848+
849+ eqs = [
850+ connect (C. output, :u , P1. input)
851+ connect (P1. output, P_mid. input)
852+ connect (P_mid. output, P2. input)
853+ connect (P2. output, :y , C. input)
854+ ]
855+ sys = System (eqs, t, systems = [P1, P_mid, P2, C], name = :cl )
856+
857+ isolated, input_vars, output_vars = isolate_subsystem (sys, :u , :y )
858+
859+ @test Set (nameof .(get_systems (isolated))) == Set ([:P1 , :P_mid , :P2 ])
860+ @test length (get_eqs (isolated)) == 2
861+ @test isequal (only (input_vars), P1. input. u)
862+ @test isequal (only (output_vars), P2. output. u)
863+ end
864+
865+ @testset " AnalysisPoint object API" begin
866+ @named P = FirstOrder (k = 1 , T = 1 )
867+ @named C = Blocks. Gain (k = - 1 )
868+
869+ eqs = [connect (C. output, :u , P. input), connect (P. output, :y , C. input)]
870+ sys = System (eqs, t, systems = [P, C], name = :cl )
871+
872+ isolated, input_vars, output_vars = isolate_subsystem (sys, sys. u, sys. y)
873+
874+ @test Set (nameof .(get_systems (isolated))) == Set ([:P ])
875+ @test isequal (only (input_vars), P. input. u)
876+ @test isequal (only (output_vars), P. output. u)
877+ end
878+
879+ @testset " causal variable connectors" begin
880+ @named P = FirstOrder (k = 1 , T = 1 )
881+ @named C = Blocks. Gain (k = - 1 )
882+
883+ eqs = [
884+ connect (C. output. u, :u , P. input. u)
885+ connect (P. output. u, :y , C. input. u)
886+ ]
887+ sys = System (eqs, t, systems = [P, C], name = :cl )
888+
889+ isolated, input_vars, output_vars = isolate_subsystem (sys, :u , :y )
890+
891+ @test Set (nameof .(get_systems (isolated))) == Set ([:P ])
892+ @test isempty (get_eqs (isolated))
893+ @test isequal (only (input_vars), P. input. u)
894+ @test isequal (only (output_vars), P. output. u)
895+ end
896+
897+ @testset " vector of symbol API" begin
898+ @named P = FirstOrder (k = 1 , T = 1 )
899+ @named C = Blocks. Gain (k = - 1 )
900+
901+ eqs = [connect (C. output, :u , P. input), connect (P. output, :y , C. input)]
902+ sys = System (eqs, t, systems = [P, C], name = :cl )
903+
904+ isolated, input_vars, output_vars = isolate_subsystem (sys, [:u ], [:y ])
905+
906+ @test Set (nameof .(get_systems (isolated))) == Set ([:P ])
907+ @test isequal (only (input_vars), P. input. u)
908+ @test isequal (only (output_vars), P. output. u)
909+ end
910+
911+ @testset " nested analysis points" begin
912+ @named P = FirstOrder (k = 1 , T = 1 )
913+ @named C = Blocks. Gain (k = - 1 )
914+
915+ # APs live inside `inner`, not at the root level
916+ inner_eqs = [connect (C. output, :u , P. input), connect (P. output, :y , C. input)]
917+ @named inner = System (inner_eqs, t, systems = [P, C])
918+ @named root = System (Equation[], t, systems = [inner])
919+
920+ # Access APs through the nested hierarchy using AnalysisPoint objects
921+ isolated, input_vars, output_vars = isolate_subsystem (
922+ root, root. inner. u, root. inner. y
923+ )
924+
925+ # root is returned; its only direct child is a trimmed inner containing only P
926+ @test Set (nameof .(get_systems (isolated))) == Set ([:inner ])
927+ inner_isolated = only (get_systems (isolated))
928+ @test Set (nameof .(get_systems (inner_isolated))) == Set ([:P ])
929+ @test isempty (get_eqs (isolated))
930+ @test isempty (get_eqs (inner_isolated))
931+ @test isequal (only (input_vars), P. input. u)
932+ @test isequal (only (output_vars), P. output. u)
933+ end
934+
935+ @testset " nested analysis points - symbol API" begin
936+ @named P = FirstOrder (k = 1 , T = 1 )
937+ @named C = Blocks. Gain (k = - 1 )
938+
939+ inner_eqs = [connect (C. output, :u , P. input), connect (P. output, :y , C. input)]
940+ @named inner = System (inner_eqs, t, systems = [P, C])
941+ @named root = System (Equation[], t, systems = [inner])
942+
943+ # Access APs by their full namespaced symbol
944+ isolated, input_vars, output_vars = isolate_subsystem (
945+ root, nameof (root. inner. u), nameof (root. inner. y)
946+ )
947+
948+ @test Set (nameof .(get_systems (isolated))) == Set ([:inner ])
949+ inner_isolated = only (get_systems (isolated))
950+ @test Set (nameof .(get_systems (inner_isolated))) == Set ([:P ])
951+ @test isequal (only (input_vars), P. input. u)
952+ @test isequal (only (output_vars), P. output. u)
953+ end
954+
955+ @testset " nested with external components at outer level" begin
956+ @named P = FirstOrder (k = 1 , T = 1 )
957+ @named C = Blocks. Gain (k = - 1 )
958+ @named ref = Step ()
959+
960+ # The APs bounding the plant live inside `inner`
961+ inner_eqs = [connect (C. output, :u , P. input), connect (P. output, :y , C. input)]
962+ @named inner = System (inner_eqs, t, systems = [P, C])
963+
964+ # `ref` exists at the outer level — it must not bleed into the isolated result
965+ outer_eqs = [connect (ref. output, inner. C. input)]
966+ @named root = System (outer_eqs, t, systems = [inner, ref])
967+
968+ isolated, input_vars, output_vars = isolate_subsystem (
969+ root, root. inner. u, root. inner. y
970+ )
971+
972+ # root is returned; ref is stripped, inner is trimmed to only P
973+ @test Set (nameof .(get_systems (isolated))) == Set ([:inner ])
974+ inner_isolated = only (get_systems (isolated))
975+ @test Set (nameof .(get_systems (inner_isolated))) == Set ([:P ])
976+ @test isempty (get_eqs (isolated))
977+ @test isempty (get_eqs (inner_isolated))
978+ @test isequal (only (input_vars), P. input. u)
979+ @test isequal (only (output_vars), P. output. u)
980+ end
981+
982+ @testset " mixed nesting levels" begin
983+ @named P = FirstOrder (k = 1 , T = 1 )
984+ @named C = Blocks. Gain (k = - 1 )
985+ @named A = Step ()
986+
987+ # AP :y lives inside `inner`; AP :u lives at root level
988+ inner_eqs = [connect (P. output, :y , C. input)]
989+ @named inner = System (inner_eqs, t, systems = [P, C])
990+
991+ # A drives P.input through AP :u at the root level
992+ outer_eqs = [connect (A. output, :u , inner. P. input)]
993+ @named root = System (outer_eqs, t, systems = [A, inner])
994+
995+ # :u is at root level, inner.y is nested — different nesting levels
996+ isolated, input_vars, output_vars = isolate_subsystem (
997+ root, :u , root. inner. y
998+ )
999+
1000+ # root is returned; A is stripped, inner is trimmed to only P (C removed)
1001+ @test Set (nameof .(get_systems (isolated))) == Set ([:inner ])
1002+ inner_isolated = only (get_systems (isolated))
1003+ @test Set (nameof .(get_systems (inner_isolated))) == Set ([:P ])
1004+ @test isempty (get_eqs (isolated))
1005+ @test isempty (get_eqs (inner_isolated))
1006+ # input_var: from root-level AP :u, connector is inner.P.input (root-namespaced)
1007+ @test isequal (only (input_vars), inner. P. input. u)
1008+ # output_var: from inner-level AP :y, connector is P.output (inner-namespaced)
1009+ @test isequal (only (output_vars), P. output. u)
1010+ end
1011+
1012+ @testset " deep nesting — isolate middle two of four" begin
1013+ @named A = Blocks. Gain (k = 1 )
1014+ @named B = FirstOrder (k = 1 , T = 1 )
1015+ @named C = FirstOrder (k = 1 , T = 2 )
1016+ @named D = Blocks. Gain (k = 1 )
1017+
1018+ # Four components in series inside `inner`; APs bound B and C (the middle two)
1019+ inner_eqs = [
1020+ connect (A. output, :ap_in , B. input),
1021+ connect (B. output, :bc , C. input),
1022+ connect (C. output, :ap_out , D. input),
1023+ ]
1024+ @named inner = System (inner_eqs, t, systems = [A, B, C, D])
1025+ @named root = System (Equation[], t, systems = [inner])
1026+
1027+ isolated, input_vars, output_vars = isolate_subsystem (
1028+ root, root. inner. ap_in, root. inner. ap_out
1029+ )
1030+
1031+ # root is returned; inner is trimmed to contain only B and C
1032+ @test Set (nameof .(get_systems (isolated))) == Set ([:inner ])
1033+ inner_isolated = only (get_systems (isolated))
1034+ @test Set (nameof .(get_systems (inner_isolated))) == Set ([:B , :C ])
1035+ # The B→C connection equation is preserved; A and D boundary APs are removed
1036+ @test length (get_eqs (inner_isolated)) == 1
1037+ @test isequal (only (input_vars), B. input. u)
1038+ @test isequal (only (output_vars), C. output. u)
1039+ # Container levels have no own variables/parameters/observed/defaults
1040+ @test isempty (get_unknowns (isolated))
1041+ @test isempty (get_ps (isolated))
1042+ @test isempty (get_unknowns (inner_isolated))
1043+ @test isempty (get_ps (inner_isolated))
1044+ end
1045+
1046+ @testset " container-level variables, parameters, and equations are stripped" begin
1047+ @named P = FirstOrder (k = 1 , T = 1 )
1048+ @named Q = FirstOrder (k = 1 , T = 2 )
1049+ @named R = FirstOrder (k = 1 , T = 3 )
1050+ @named S = Blocks. Gain (k = 1 )
1051+
1052+ # Declare an extra variable and parameter at the inner (container) level
1053+ @variables extra_state (t) = 0.0
1054+ @parameters extra_gain = 2.0
1055+
1056+ inner_eqs = [
1057+ connect (P. output, :ap_in , Q. input),
1058+ connect (Q. output, :qr , R. input),
1059+ connect (R. output, :ap_out , S. input),
1060+ # Plain algebraic equation declared at the container level — must be removed
1061+ extra_state ~ extra_gain * Q. output. u,
1062+ ]
1063+ # inner has its own unknowns, ps, defaults, and a non-connection equation
1064+ inner = System (
1065+ inner_eqs, t, [extra_state], [extra_gain];
1066+ name = :inner , systems = [P, Q, R, S]
1067+ )
1068+ @named root = System (Equation[], t, systems = [inner])
1069+
1070+ isolated, input_vars, output_vars = isolate_subsystem (
1071+ root, root. inner. ap_in, root. inner. ap_out
1072+ )
1073+
1074+ inner_isolated = only (get_systems (isolated))
1075+ @test Set (nameof .(get_systems (inner_isolated))) == Set ([:Q , :R ])
1076+ # The Q→R connection AP is kept; extra_state equation and boundary APs are removed
1077+ @test length (get_eqs (inner_isolated)) == 1
1078+ # extra_state, extra_gain, their defaults, and the algebraic equation are stripped
1079+ @test isempty (get_unknowns (inner_isolated))
1080+ @test isempty (get_ps (inner_isolated))
1081+ @test isempty (get_observed (inner_isolated))
1082+ @test isempty (get_defaults (inner_isolated))
1083+ # root is also clean
1084+ @test isempty (get_unknowns (isolated))
1085+ @test isempty (get_ps (isolated))
1086+ end
1087+ end
7821088end
7831089
7841090using DynamicQuantities
0 commit comments