JuliaControl / RobustAndOptimalControl.jl

Robust and optimal design and analysis of linear control systems

Home Page:https://juliacontrol.github.io/RobustAndOptimalControl.jl/dev/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Hard to understand error message: Lengths of U2 (1:27) and Y1

tallakt opened this issue · comments

So I was working on my first larger block diagram, and trying to get everything in place. First I was meeting error messages giving me the name of an input/output that I was able to figure out. But after sorting out all that, I was presented with the following error message, after which I am quite stuck.

I am asking in this issue if maybe the error message should be improved on?

julia> bd = mk2_block_diagram_()
┌ Warning: Connecting single output to multiple inputs Y1=Union{Nothing, Int64}[1, 9, 8, 5, 20, 21, 22, 23, 24, 25, 11, 13, 12, 14, 3, 4, 6, 7, 16, 18, 17, 19, 15, 4, 11, 12, 21, 2, 10, 1]
└ @ ControlSystemsBase ~/.julia/packages/ControlSystemsBase/eWEYr/src/connections.jl:323
ERROR: Lengths of U2 (1:27) and Y1 (Union{Nothing, Int64}[1, 9, 8, 5, 20, 21, 22, 23, 24, 25, 11, 13, 12, 14, 3, 4, 6, 7, 16, 18, 17, 19, 15, 4, 11, 12, 21, 2, 10, 1]) must be equal
Stacktrace:
 [1] error(s::String)
   @ Base ./error.jl:35
 [2] feedback(sys1::StateSpace{Continuous, Float64}, sys2::StateSpace{Continuous, Bool}; U1::Vector{Union{Nothing, Int64}}, Y1::Vector{Union{Nothing, Int64}}, U2::UnitRange{Int64}, Y2::UnitRange{Int64}, W1::Vector{Union{Nothing, Int64}}, Z1::Vector{Union{Nothing, Int64}}, W2::Vector{Union{Nothing, Int64}}, Z2::Vector{Union{Nothing, Int64}}, Wperm::Colon, Zperm::Colon, pos_feedback::Bool)
   @ ControlSystemsBase ~/.julia/packages/ControlSystemsBase/eWEYr/src/connections.jl:332
 [3] feedback(s1::NamedStateSpace{Continuous, StateSpace{Continuous, Float64}}, s2::NamedStateSpace{Continuous, StateSpace{Continuous, Bool}}; u1::Vector{Symbol}, w1::Vector{Symbol}, z1::Vector{Symbol}, y1::Vector{Symbol}, u2::Function, y2::Function, w2::Vector{Any}, z2::Vector{Any}, kwargs::Base.Pairs{Symbol, Bool, Tuple{Symbol}, NamedTuple{(:pos_feedback,), Tuple{Bool}}})
   @ RobustAndOptimalControl ~/.julia/packages/RobustAndOptimalControl/nd5G8/src/named_systems2.jl:367
 [4] connect(systems::Vector{NamedStateSpace{Continuous, StateSpace{Continuous, Float64}}}; u1::Vector{Symbol}, y1::Vector{Symbol}, w1::Vector{Symbol}, z1::Vector{Symbol}, verbose::Bool, kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
   @ RobustAndOptimalControl ~/.julia/packages/RobustAndOptimalControl/nd5G8/src/named_systems2.jl:463
 [5] connect(systems::Vector{NamedStateSpace{Continuous, StateSpace{Continuous, Float64}}}, pairs::Vector{Pair{Symbol, Symbol}}; kwargs::Base.Pairs{Symbol, Vector{Symbol}, Tuple{Symbol, Symbol}, NamedTuple{(:w1, :z1), Tuple{Vector{Symbol}, Vector{Symbol}}}})
   @ RobustAndOptimalControl ~/.julia/packages/RobustAndOptimalControl/nd5G8/src/named_systems2.jl:467
 [6] mk2_block_diagram_(; inner_feedback::Bool, outer_feedback::Bool)
   @ Main ~/Documents/forsøk/spark_10_feedback/spark_10_feedback.jl:386
 [7] mk2_block_diagram_()
   @ Main ~/Documents/forsøk/spark_10_feedback/spark_10_feedback.jl:327
 [8] top-level scope
   @ REPL[256]:1

The code for the failing function:

function mk2_block_diagram(airspeed; params = init_pitch_params(), inner_feedback = true, outer_feedback = true)
  integrator = tf([1], [1, 0])
  dynamic_pressure = 0.5 * params.rho * airspeed^2
  k0 = params.aoa_to_moment_gain_normalized
  k1 = params.aoa_rate_to_moment_gain_normalized
  back = dynamic_pressure * parallel(k0 * integrator, tf(k1 / max(airspeed, eps())))
  # we assume the airflow is coming from pitch angle zero
  feedback(inertia_tf(params = params), back)
  
  my_named_tf = (tf, name) -> named_ss(ss(tf), u = Symbol("$(name)_u"), x = Symbol("$(name)_x"), y = Symbol("$(name)_y"))
  only_first = arr -> [x[1] => y[1] for (x, y) = arr]

  inertia = my_named_tf(inertia_tf(), :inertia) # output is pitch rate
  pitch_int = my_named_tf(integrator, :pitch_int) # output is pitch angle
  aoa_aero = my_named_tf(tf(dynamic_pressure * k0), :aoa_aero)
  aoa_rate_aero = my_named_tf(tf(dynamic_pressure * k1 / max(eps(), airspeed)), :aoa_rate_aero)
  aoa_int = my_named_tf(integrator, :aoa_int) # output is aoa angle, signal is aoa_rate otherwise system is not proper
  hacker = my_named_tf(hacker_tf(), :hacker)
  elev_aero = my_named_tf(elevator_to_aero_moments_tf(airspeed, params = params), :elev_aero)
  props_ctrl = my_named_tf(mk2_inner_props_controller(), :props_ctrl)
  elev_ctrl = my_named_tf(mk2_inner_elev_controller(), :elev_ctrl)
  outer_ctrl = my_named_tf(mk2_outer_controller_tf(), :outer_ctrl)

  inputs = [:pitch_ref
            , :aoa_rate
            , :disturbance
           ]

  blocks = [
            inertia
            , pitch_int
            , aoa_aero
            , aoa_rate_aero
            , aoa_int
            , hacker
            , elev_aero
            , props_ctrl
            , elev_ctrl
            , outer_ctrl
           ]

  splitters = [
               splitter(:aoa_rate, 2)
               , splitter(:pitch_int_y, 3)
               , splitter(:outer_ctrl_y, 2)
               , splitter(:inertia_y, 2)
              ]

  sum_blocks = [
                sumblock("aoa_int_u = aoa_rate1 - pitch_int_y1")
                , sumblock("aoa_rate_aero_u = aoa_rate2 - pitch_int_y2")
                , sumblock("inertia_u = aoa_aero_y + aoa_rate_aero_y + hacker_y + elev_aero_y + disturbance")
                , sumblock(inner_feedback ? "props_ctrl_u = outer_ctrl_y1 - inertia_y1" : "props_ctrl_u = outer_ctrl_y1")
                , sumblock(inner_feedback ? "elev_ctrl_u = outer_ctrl_y2 - inertia_y2" : "elev_ctrl_u = outer_ctrl_y2" )
                , sumblock(outer_feedback ? "outer_ctrl_u = pitch_ref - pitch_int_y3" :  "outer_ctrl_u = pitch_ref")
               ]

  connections = vcat(
                     only_first([elev_ctrl.y => elev_aero.u
                                 , props_ctrl.y => hacker.u
                                 , aoa_int.y => aoa_aero.u
                                ])
                     , [sys.y[1] => sys.y[1] for sys = sum_blocks] # need to connect sum blocks to real blocks
                     , vcat([[n => n for n = setdiff(sys.u, inputs)] for sys = sum_blocks]...)
                     , vcat([sys.u[1] => sys.u[1] for sys = splitters])
                     , :inertia_y3 => :pitch_int_u
                    )
  # dump([
  # :blocks => blocks
  # , :splitters => splitters
  # , :sum_blocks => sum_blocks
  # , :connections => connections
  # ])
  connect(vcat(blocks, splitters, sum_blocks), connections, w1 = inputs, z1 = [sys.y[1] for sys = blocks])
end

I could also mention a suggestion for improving the user experience by leaving less of the plumbing to the end user. The code above could instead look like:

function mk2_block_diagram_(; inner_feedback = true, outer_feedback = true)
  inertia = tag_sys(dummy_tf)
  pitch_int = tag_sys(dummy_tf)
  aoa_aero = tag_sys(dummy_tf)
  aoa_rate_aero = tag_sys(dummy_tf)
  aoa_int = tag_sys(dummy_tf)
  hacker = tag_sys(dummy_tf)
  elev_aero = tag_sys(dummy_tf)
  props_ctrl = tag_sys(dummy_tf)
  elev_ctrl = tag_sys(dummy_tf)
  outer_ctrl = tag_sys(dummy_tf)

  error_aoa_rate = summation(:aoa_rate, (:-, inertia))
  error_pitch_rate = summation(outer_ctrl, (:-, inertia))

  bd = initblockdiagram()
  bd = connect(bd, :moment, inertia, :pitch_rate, pitch_int, :pitch)
  bd = connect(bd, summation(
                             (:-, aoa_aero)
                             , (:-, aoa_rate_aero)
                             , hacker
                             , elev_aero
                             , :disturbance
                            )
               , inertia)
  bd = connect(bd, error_aoa_rate, aoa_rate_aero)
  bd = connect(bd, aoa_int, aoa_aero)
  bd = connect(bd, error_aoa_rate, aoa_int)
  bd = connect(bd, :elev_sp, elev_ctrl, elev_aero)
  bd = connect(bd, :prop_moment_sp, props_ctrl, hacker)
  bd = inner_feedback ? connect(bd, error_pitch_rate, elev_ctrl ) : bd
  bd = inner_feedback ? connect(bd, error_pitch_rate, props_ctrl) : bd
  bd = outer_feedback ? connect(bd, summation(:pitch_ref, (:-, :pitch)), outer_ctrl) : bd

  # now use the block diagram graph to create a system
  sys0 = blockdiagramtosys(bd, :pitch_ref, pitch_int)

  # or alternatively
  sys1 = blockdiagramtosys(bd, :pitch_ref, :pitch)
  sys2 = blockdiagramtosys(bd, :pitch_ref, :pitch)

  # then use it...
  bodeplot(sys0)
end

Edit: after giving this a bit more though, I further simplified the API. Now just get rid of all the named and then drop in tags at any appropriate place in the connect calls. A tag can later become an input or an output, depending on how it's used in the blockdiagramtosys function

That could be a bug in connect, are you sure you have connections to all the blocks that are listed in

vcat(blocks, splitters, sum_blocks)

?

That could be a bug in connect, are you sure you have connections to all the blocks that are listed in

vcat(blocks, splitters, sum_blocks)

I went through all inputs and outputs in more detail and found I believe two bugs. Though fixing those did not seem to change the outcome. I have edited the original code.