Skip to content

Commit 3d3a559

Browse files
committed
smooth curve offsets to avoid discontinuities
1 parent 6da8706 commit 3d3a559

File tree

1 file changed

+79
-11
lines changed

1 file changed

+79
-11
lines changed

control/freqplot.py

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -519,8 +519,8 @@ def gen_zero_centered_series(val_min, val_max, period):
519519

520520
# Default values for module parameter variables
521521
_nyquist_defaults = {
522-
'nyquist.primary_style': ['-', ':'], # style for primary curve
523-
'nyquist.mirror_style': ['--', '-.'], # style for mirror curve
522+
'nyquist.primary_style': ['-', '-.'], # style for primary curve
523+
'nyquist.mirror_style': ['--', ':'], # style for mirror curve
524524
'nyquist.arrows': 2,
525525
'nyquist.arrow_size': 8,
526526
'nyquist.indent_radius': 1e-6, # indentation radius
@@ -696,6 +696,7 @@ def nyquist_plot(syslist, omega=None, plot=True, omega_limits=None,
696696
kwargs.pop('arrow_length', False)
697697

698698
# Get values for params (and pop from list to allow keyword use in plot)
699+
omega_num_given = omega_num is not None
699700
omega_num = config._get_param('freqplot', 'number_of_samples', omega_num)
700701
arrows = config._get_param(
701702
'nyquist', 'arrows', kwargs, _nyquist_defaults, pop=True)
@@ -736,8 +737,13 @@ def _parse_linestyle(style_name, allow_false=False):
736737
omega, omega_range_given = _determine_omega_vector(
737738
syslist, omega, omega_limits, omega_num)
738739
if not omega_range_given:
739-
# Start contour at zero frequency
740-
omega[0] = 0.
740+
if omega_num_given:
741+
# Just reset the starting point
742+
omega[0] = 0.0
743+
else:
744+
# Insert points between the origin and the first frequency point
745+
omega = np.concatenate((
746+
np.linspace(0, omega[0], indent_points), omega[1:]))
741747

742748
# Go through each system and keep track of the results
743749
counts, contours = [], []
@@ -938,17 +944,23 @@ def _parse_linestyle(style_name, allow_false=False):
938944
x_reg, y_reg, primary_style[0], color=color, *args, **kwargs)
939945
c = p[0].get_color()
940946

947+
# Figure out how much to offset the curve: the offset goes from
948+
# zero at the start of the scaled section to max_curve_offset as
949+
# we move along the curve
950+
curve_offset = _compute_curve_offset(
951+
resp, scale_mask, max_curve_offset)
952+
941953
# Plot the scaled sections of the curve (changing linestyle)
942954
x_scl = np.ma.masked_where(scale_mask, resp.real)
943955
y_scl = np.ma.masked_where(scale_mask, resp.imag)
944956
plt.plot(
945-
x_scl * (1 + max_curve_offset), y_scl * (1 + max_curve_offset),
957+
x_scl * (1 + curve_offset), y_scl * (1 + curve_offset),
946958
primary_style[1], color=c, *args, **kwargs)
947959

948960
# Plot the primary curve (invisible) for setting arrows
949961
x, y = resp.real.copy(), resp.imag.copy()
950-
x[reg_mask] *= (1 + max_curve_offset)
951-
y[reg_mask] *= (1 + max_curve_offset)
962+
x[reg_mask] *= (1 + curve_offset[reg_mask])
963+
y[reg_mask] *= (1 + curve_offset[reg_mask])
952964
p = plt.plot(x, y, linestyle='None', color=c, *args, **kwargs)
953965

954966
# Add arrows
@@ -962,14 +974,14 @@ def _parse_linestyle(style_name, allow_false=False):
962974
plt.plot(
963975
x_reg, -y_reg, mirror_style[0], color=c, *args, **kwargs)
964976
plt.plot(
965-
x_scl * (1 - max_curve_offset),
966-
-y_scl * (1 - max_curve_offset),
977+
x_scl * (1 - curve_offset),
978+
-y_scl * (1 - curve_offset),
967979
mirror_style[1], color=c, *args, **kwargs)
968980

969981
# Add the arrows (on top of an invisible contour)
970982
x, y = resp.real.copy(), resp.imag.copy()
971-
x[reg_mask] *= (1 - max_curve_offset)
972-
y[reg_mask] *= (1 - max_curve_offset)
983+
x[reg_mask] *= (1 - curve_offset[reg_mask])
984+
y[reg_mask] *= (1 - curve_offset[reg_mask])
973985
p = plt.plot(x, -y, linestyle='None', color=c, *args, **kwargs)
974986
_add_arrows_to_line2D(
975987
ax, p[0], arrow_pos, arrowstyle=arrow_style, dir=-1)
@@ -1087,6 +1099,62 @@ def _add_arrows_to_line2D(
10871099
arrows.append(p)
10881100
return arrows
10891101

1102+
#
1103+
# Function to compute Nyquist curve offsets
1104+
#
1105+
# This function computes a smoothly varying offset that starts and ends at
1106+
# zero at the ends of a scaled segment.
1107+
#
1108+
def _compute_curve_offset(resp, mask, max_offset):
1109+
# Compute the arc length along the curve
1110+
s_curve = np.cumsum(
1111+
np.sqrt(np.diff(resp.real) ** 2 + np.diff(resp.imag) ** 2))
1112+
1113+
# Initialize the offset
1114+
offset = np.zeros(resp.size)
1115+
arclen = np.zeros(resp.size)
1116+
1117+
# Walk through the response and keep track of each continous component
1118+
i, nsegs = 0, 0
1119+
while i < resp.size:
1120+
# Skip the regular segment
1121+
while i < resp.size and mask[i]:
1122+
i += 1 # Increment the counter
1123+
if i == resp.size:
1124+
break;
1125+
# Keep track of the arclength
1126+
arclen[i] = arclen[i-1] + np.abs(resp[i] - resp[i-1])
1127+
1128+
nsegs += 0.5
1129+
if i == resp.size:
1130+
break;
1131+
1132+
# Save the starting offset of this segment
1133+
seg_start = i
1134+
1135+
# Walk through the scaled segment
1136+
while i < resp.size and not mask[i]:
1137+
i += 1
1138+
if i == resp.size: # See if we are done with this segment
1139+
break;
1140+
# Keep track of the arclength
1141+
arclen[i] = arclen[i-1] + np.abs(resp[i] - resp[i-1])
1142+
1143+
nsegs += 0.5
1144+
if i == resp.size:
1145+
break;
1146+
1147+
# Save the ending offset of this segment
1148+
seg_end = i
1149+
1150+
# Now compute the scaling for this segment
1151+
s_segment = arclen[seg_end-1] - arclen[seg_start]
1152+
offset[seg_start:seg_end] = max_offset * s_segment/s_curve[-1] * \
1153+
np.sin(np.pi * (arclen[seg_start:seg_end]
1154+
- arclen[seg_start])/s_segment)
1155+
1156+
return offset
1157+
10901158

10911159
#
10921160
# Gang of Four plot

0 commit comments

Comments
 (0)