Bodeplot with XLim option does not rescale y-axis in R2024b

Hi,
I am using MATLAB on ubuntu 24.04. I recently updated from MATLAB 24a to 24b. After the update I have the following problem:
I use bodeoptions() (from control systems toolbox) with XLim option set to the frequency range of bodeplot(). I want the y-axis the be auto scaled to the data. It worked like this in R24a, however now the y scale is not updated for the XLim which is set. This is quite anoying as I always have to zoom in on the plots. How can I fix this?
To recreate run:
s = tf("s");
G = 1/s*exp(-0.1*s);
opts = bodeoptions;
opts.XLim = [1, 100];
bodeplot(G, opts);
Then one can see that both the phase and magnitude y-axes should be rescaled.

 Accepted Answer

Hello Tor,
This is a side effect of the update to charts in R2024b. The chart's Y limits are no longer linked to its X limits. To achieve the effect you desire, you should instead limit the frequency range of the response data like below.
s = tf("s");
G = 1/s*exp(-0.1*s);
bodeplot(G, {1 100});

11 Comments

Thank you! Though, I would like to point out that In the following example opts.XLim overwrites {0.1, 1e4}. And we are back to the same problem.
opts = bodeoptions;
opts.XLim = [0.1, 1e1];
s = tf("s");
G = 1/(0.1*s + 1);
bodeplot(G, {0.1, 1e4}, opts);
To fix this and make it behave more like in R2024a I made my own bodeplot function in following code. This works for my usecases at least.
opts = bodeoptions;
opts.XLim = [0.1, 1e4];
s = tf("s");
G = 1/(0.1*s + 1);
my_bodeplot(G, opts);
function my_bodeplot(sys, options)
arguments
sys
options = bodeoptions
end
if strcmp(options.XLimMode{:}, 'manual')
freqs = options.XLim{:};
if strcmp(options.FreqUnits, 'Hz')
freqs = 2*pi.*freqs;
end
freqs = num2cell(freqs);
options.XLimMode = 'auto';
bodeplot(sys, freqs, options);
else
bodeplot(sys, options)
end
end
I suggest reporting this as a bug.
I feel as though I may have not been sufficently clear, so let me try again.
The syntax bodeplot(sys,w) will sample the frequency response of sys at the frequencies given by w, which in turn will affect the automatic XLimits and YLimits chosen by the chart.
However, you can still override these automatic limits by passing in a bodeoptions object, in which case the chart will appear with any manual limits specified by that object.
Your custom function my_bodeplot is largely redundant and possibly incomplete, operating under the assumption you are solely using it to set initial XLimits (which may be an underestimation). See my annotations below.
function my_bodeplot(sys, options)
arguments
sys
options = bodeoptions %bodeoptions('cstprefs') will load your user preferences
end
if strcmp(options.XLimMode{:}, 'manual') %options.XLimMode{:} could produce multiple outputs for MIMO systems
freqs = options.XLim{:}; %same as above
if strcmp(options.FreqUnits, 'Hz') %does not account for other frequency units
freqs = 2*pi.*freqs; %assumes XLim is provided in rad/s. By default, the FrequencyUnit of BodePlot is chosen as rad/TimeUnit of its first system
end
freqs = num2cell(freqs); %similar MIMO issue
options.XLimMode = 'auto'; %this means your custom XLimits will be ignored, essentially reverting bodeoptions to default
bodeplot(sys, freqs, options); %options has no effect if default
else
bodeplot(sys, options) %options has no effect if default
end
end
I would recommend forgoing the passing of an options object to the constructor entirely. Instead, I invite you to explore the new BodePlot API, and set properties on the chart handle as needed after construction. Hopefully, it is a bit clearer how you are affecting the chart without having to guess what the chart will look like.
For completeness, the properties of interest in this discussion map as follows, where h is the BodePlot object outputted by bodeplot():
w -> h.Responses(idx).SourceData.FrequencySpec
bodeoptions.XLim -> h.XLimits
bodeoptions.YLim -> h.YLimits
So to summarize, this behavior is working as intended. We changed the behavior of YLimits to make it independent of XLimits with the change to charts in R2024b, which in my opinion is a logical change. Why should the chart change its YLimits when you only asked it to change its XLimits?
"Why should the chart change its YLimits when you only asked it to change its XLimits?"
1) Because the YLimMode is 'auto'. What else would that mean other than to cover the full span of the plot over the specified XLim? Having said that, it's not clear from the doc page (in 2024b and previous versions) whether YLim and YLimMode is supposed apply to the magnitude plot, the phase plot, or both.
2. Because, as the OP pointed out, the current behavior is a change from how things worked in previous versions.
Also, why does specifying the XLim cause the plots to have different fonts for the titles and axis labels?
s = tf("s");
G = 1/s*exp(-0.1*s);
figure
bodeplot(G)
opts = bodeoptions;
opts.XLim = [1, 100];
figure
h = bodeplot(G, opts);
Thank you for your clarification @Andrew Ouellette. I am aweare that my function only works for my specific usecase. I will take a look at the new bodeplot API you linked to.
I must say I agree with @Paul. The main source of my confusion and iritation was that the behaviour has changed. I had a setup which formatted my figures and now I have to change it. Not to veer of topic, but 'Interpreter' 'latex' is broken for legends:
s = tf("s");
G = 1/s;
bodeplot(G);
hold on;
bodeplot(2*G);
legend({"G1", "G2"}, 'Interpreter', 'latex');
Error using legend (line 176)
Conversion from cell failed. Element 1 must be convertible to a string scalar.
This means that my previous code does not even run... :) I did report a bug for this also, however I can't seem to find the report again.
Also think it makes sens that "auto" means that the y-scale will fit the data within the x-limits. If this is not the intended behaviour, could you then explain how I would force a rescale of the y-axis to fit the data without having to change how you call bodeplot()?
Thanks for the feedback. Let me try to address your concerns.
1a) What does YLimitsMode="auto" mean?
This question has a different answer depending on the release (pre or post R2024b). Previously, the automatic YLimits were chosen based on the porition of the plotted data displayed within the chosen XLimits. This behavior was never explicitly documented. Now, the BodePlot object uses the entire range of the plotted data to choose its automatic YLimits, regardless which portion is displayed due to choice of XLimits (see YLimitsMode). This behavior is now documented.
1b) How do limits properties map to chart axes?
This question points to a pitfall with the plot options objects. The best answer I can give here is that the chart does its best to interpret which axes to apply the limits from the plot options object. The new chart API is much more explicit to which axes the limits apply, as it knows many axes it contains.
2) Why does this behavior differ from previous releases?
R2024b saw the introduction of chart objects into Control System Toolbox, which caused changes in certain behaviors. The previous response plots were inconsistent with how they treated limits, so we took the oppportunity to standardize the behavior across our charts. We chose the standardization that is most consistent with chart interactions like zooming or panning.
3) Why does using the plot options object to specify XLimits change other properties?
The default constructor bodeoptions does not take user preferences into account, whereas the default constructor for bodeplot does. You can use bodeoptions('cstprefs') to map your user preferences to the plot options object. However, I would encourage you to try setting XLimits using the chart handle after construction instead, as it likely is a more user-friendly workflow to see your chart first!
1) Why does specifying the legend interpreter cause an error?
You are correct here, this does appear to be a bug.
2) How can I rescale the y axis to fit the data without changing the call to bodeplot()?
To reiterate, the previous behavior scaled the y axis to fit the portion of the plotted data visible due to the chosen x limits rather than the entire plotted ata. The current behavior now scales the y axis to fit the entire plotted data, regardless of which portion is visible due to the chosen x limits. The syntax I proposed that you try, bodeplot(sys,w), changes which data is plotted based on the frequency specification, and therefore the automatic picking of limits. I understand that it is a pain to change previously functional code; however, from what information you've provided here, this syntax does appear to match your desired behavior.
legend({"G1", "G2"}, 'Interpreter', 'latex');
This is invalid syntax. You need to pass either a cell array of character vectors, or else a string array.
legend(["G1", "G2"], 'Interpreter', 'latex');
legend({'G1', 'G2'}, 'Interpreter', 'latex');
Regarding answer (1)
"This question has a different answer depending on the release (pre or post R2024b)."
Therein lies the rub. I'm sure this decision wasn't undertaken lightly, but still can cause confusion.
"Previously, the automatic YLimits were chosen based on the porition of the plotted data displayed within the chosen XLimits. This behavior was never explicitly documented. Now, the BodePlot object uses the entire range of the plotted data to choose its automatic YLimits, regardless which portion is displayed due to choice of XLimits (see YLimitsMode). This behavior is now documented."
Two issues to unpack here.
First, it seems like there is a distinction between plotted data and displayed data. Now that you mention it, I do seem to recall that bode computes more data than it actually displays. Maybe I'm wrong about that. I'm not sure what the difference is between plotted and displayed data. Regardless, from a user perspective there's no difference between what data is plotted and what data is displayed. The plot is the display and the display is the plot. At this point, I have absolutely no idea what the meaning is of "the entire range of the plotted data."
Second, the documentation for YLimMode = 'auto' from 2024a is identical to YLimitsMode in 2024b.
2024a: " 'auto' — Enable automatic limit selection, which is based on the total span of the plotted data."
2024b: "'auto' — Enable automatic limit selection, which is based on the total span of the plotted data."
So both versions have the same clarity, or lack thereof.
As for item (3) ....
I still don't understand why changing the XLim field of the bodeptions and using the result to then call bodeplot() should cause a change to title and label fonts (regardless of any issues with span of the y-axes limits).
I don't know if that call to legend is an illegal syntax, but the error message makes no sense:
"Conversion from cell failed. Element 1 must be convertible to a string scalar."
But Element 1 IS a string scalar, is it not?
G = {"G1","G2"};
all([isstring(G{1}), isscalar(G{1})])
ans = logical
1
And that syntax with legend does work in the context of an ordinary plot
figure
plot(rand(2))
legend({"G1","G2"});
Hi @Paul,
There is a difference for users between plotted and displayed data.You are correct that more data is plotted than what is displayed by default. You can see this additional data by panning or zooming out on the chart, which is equivalent to changing the XLimits and/or YLimits properties.
To explain the vagueness of "the entire range of the plotted data," let's take a simple integrator system (1/s) as an example. At w=0, the gain is infinite, and at w=Inf, the gain is 0 (or -Inf dB). Clearly, we can't plot the frequency response over the entire region [0,Inf] x [-Inf,Inf] on any finite computer screen. Instead, we use either the user-provided frequency specification or some heuristics to pick an "interesting" frequency range [wmin,wmax] and then compute the corresponding minimum/maximum gains & phases in that interval. Those become the automatic limit values corresponding to "the entire range of the plotted data". Although, we do still plot the frequency response at frequency points beyond that range by default, so perhaps the "the entire range of the plotted data" is a bit misleading.
As to why using bodeoptions sets more properties than just the XLimits, the bodeoptions object does not track which properties a user has modified. The chart has no choice but to take the entire object at "face value", and map each of the options properties to the corresponding chart property. This causes the chart fonts to change if you did not create the bodeoptions object using the 'cstprefs' flag, since the chart applies the user preferences by default unlike the options object.
Lastly, Tor is correct in that the legend error results from the 'Interpreter' name-value pair being specified. Walter is right in that we do not explictly document passing legend names using cell arrays of strings (we only document cell arrays of character arrays and string arrays), but currently our input parser is able to account for the undocumented cell array of strings case.
I think I'm following all of this, though I think it would be better to not use "plotted" and "displayed" to mean two very different things. Perhaps "computed" and "displayed" would be better terminology. So I guess that setting the XLim now has no bearing on the limits of the computed data, which are now based only on the user-specified [wmin,wmax] or the heuristic. Still unclear to me how the limits are selected for the displayed data (if not specified by the user) insofar as the displayed data has different limits than the computed data.
I'm still not following the font changes. Why aren't the three cases below the same?
My image is that Case 1 uses the default plotoptions because no other was specified. Is that not the case? If it isn't, then what plotoptions are being used in Case 1?
Case 2 uses the default plotoptions explicitly, which should be the same as Case 1(from a user perspective), but it isn't.
Case 3 uses the "the plot options with the options you selected in the Control System Toolbox™ ... preferences editor." But I haven't run ctrlpref here, so it seems like I should be just getting back the same plotoptions object as used for Case 2. But, Case 3 is actually the same as Case 1.
From a user perspective, based on my current understanding, I think that all three of these cases should yield the same results as they should all be based on the same default settings, whatever they may be.
s = tf("s");
G = 1/s*exp(-0.1*s);
% Case 1
figure
bodeplot(G)
% Case 2
figure
bodeplot(G,bodeoptions)
% Case 3
figure
bodeplot(G,bodeoptions('cstprefs'))
If I had changed the local preferences using ctrlpref, then I would expect that those would be used for all three of these cases. As it stands, it sounds like if I do have local preferences defined (after using ctrlpref), they are completely ignored unless I force them into action using bodeoptions('cstprefs'), which seems counter to the whole point of setting preferences via ctrlpref in the first place.
As it turns out, the defaults for bodeoptions are not the same as the defaults for bodeoptions('cstprefs')
opts1 = bodeoptions;
opts2 = bodeoptions('cstprefs');
isequal(opts1,opts2)
ans = logical
0
opts1.Title
ans = struct with fields:
String: 'Bode Diagram' FontSize: 8 FontWeight: 'Normal' FontAngle: 'Normal' Color: [0 0 0] Interpreter: 'tex'
opts2.Title
ans = struct with fields:
String: 'Bode Diagram' FontSize: 11 FontWeight: 'bold' FontAngle: 'normal' Color: [0 0 0] Interpreter: 'tex'
So the explains the difference between Case 2 and Case 3, and Case 1 looks like it uses bodeoptions('cstprefs') under the hood. But that naturally leads to the question of why bodeoptions and bodeoptions('cstprefs') return different results when no action was taken to modify the default preferences in the first place.

Sign in to comment.

More Answers (0)

Products

Release

R2024b

Asked:

on 7 Feb 2025

Edited:

on 11 Feb 2025

Community Treasure Hunt

Find the treasures in MATLAB Central and discover how the community can help you!

Start Hunting!