-
-
Notifications
You must be signed in to change notification settings - Fork 7.9k
TickedStroke, a stroke style with ticks useful for depicting constraints #15458
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
""" | ||
============================================== | ||
Contouring the solution space of optimizations | ||
============================================== | ||
|
||
Contour plotting is particularly handy when illustrating the solution | ||
space of optimization problems. Not only can `.axes.Axes.contour` be | ||
used to represent the topography of the objective function, it can be | ||
used to generate boundary curves of the constraint functions. The | ||
constraint lines can be drawn with | ||
`~matplotlib.patheffects.TickedStroke` to distinguish the valid and | ||
invalid sides of the constraint boundaries. | ||
|
||
`.axes.Axes.contour` generates curves with larger values to the left | ||
of the contour. The angle parameter is measured zero ahead with | ||
increasing values to the left. Consequently, when using | ||
`~matplotlib.patheffects.TickedStroke` to illustrate a constraint in | ||
a typical optimization problem, the angle should be set between | ||
zero and 180 degrees. | ||
|
||
""" | ||
|
||
import numpy as np | ||
import matplotlib.pyplot as plt | ||
import matplotlib.patheffects as patheffects | ||
|
||
fig, ax = plt.subplots(figsize=(6, 6)) | ||
|
||
nx = 101 | ||
ny = 105 | ||
|
||
# Set up survey vectors | ||
xvec = np.linspace(0.001, 4.0, nx) | ||
yvec = np.linspace(0.001, 4.0, ny) | ||
|
||
# Set up survey matrices. Design disk loading and gear ratio. | ||
x1, x2 = np.meshgrid(xvec, yvec) | ||
|
||
# Evaluate some stuff to plot | ||
obj = x1**2 + x2**2 - 2*x1 - 2*x2 + 2 | ||
g1 = -(3*x1 + x2 - 5.5) | ||
g2 = -(x1 + 2*x2 - 4) | ||
g3 = 0.8 + x1**-3 - x2 | ||
|
||
cntr = ax.contour(x1, x2, obj, [0.01, 0.1, 0.5, 1, 2, 4, 8, 16], | ||
colors=('k',)) | ||
ax.clabel(cntr, fmt="%2.1f", use_clabeltext=True) | ||
|
||
cg1 = ax.contour(x1, x2, g1, [0], colors=('k',)) | ||
plt.setp(cg1.collections, | ||
path_effects=[patheffects.withTickedStroke(angle=135)]) | ||
|
||
cg2 = ax.contour(x1, x2, g2, [0], colors=('r',)) | ||
plt.setp(cg2.collections, | ||
path_effects=[patheffects.withTickedStroke(angle=60, length=2)]) | ||
|
||
cg3 = ax.contour(x1, x2, g3, [0], colors=('b',)) | ||
plt.setp(cg3.collections, | ||
path_effects=[patheffects.withTickedStroke(spacing=7)]) | ||
|
||
ax.set_xlim(0, 4) | ||
ax.set_ylim(0, 4) | ||
|
||
plt.show() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
""" | ||
============================== | ||
Lines with a ticked patheffect | ||
============================== | ||
|
||
Ticks can be added along a line to mark one side as a barrier using | ||
`~matplotlib.patheffects.TickedStroke`. You can control the angle, | ||
spacing, and length of the ticks. | ||
|
||
The ticks will also appear appropriately in the legend. | ||
|
||
""" | ||
|
||
import numpy as np | ||
import matplotlib.pyplot as plt | ||
from matplotlib import patheffects | ||
|
||
fig, ax = plt.subplots(figsize=(6, 6)) | ||
ax.plot([0, 1], [0, 1], label="Line", | ||
path_effects=[patheffects.withTickedStroke(spacing=7, angle=135)]) | ||
|
||
nx = 101 | ||
x = np.linspace(0.0, 1.0, nx) | ||
y = 0.3*np.sin(x*8) + 0.4 | ||
ax.plot(x, y, label="Curve", path_effects=[patheffects.withTickedStroke()]) | ||
|
||
ax.legend() | ||
|
||
plt.show() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
""" | ||
ramcdona marked this conversation as resolved.
Show resolved
Hide resolved
|
||
======================= | ||
TickedStroke patheffect | ||
======================= | ||
|
||
Matplotlib's :mod:`.patheffects` can be used to alter the way paths | ||
are drawn at a low enough level that they can affect almost anything. | ||
|
||
The :doc:`patheffects guide</tutorials/advanced/patheffects_guide>` | ||
details the use of patheffects. | ||
|
||
The `~matplotlib.patheffects.TickedStroke` patheffect illustrated here | ||
draws a path with a ticked style. The spacing, length, and angle of | ||
ticks can be controlled. | ||
|
||
See also the :doc:`contour demo example | ||
</gallery/lines_bars_and_markers/lines_with_ticks_demo>`. | ||
|
||
See also the :doc:`contours in optimization example | ||
</gallery/images_contours_and_fields/contours_in_optimization_demo>`. | ||
""" | ||
|
||
import matplotlib.patches as patches | ||
from matplotlib.path import Path | ||
import numpy as np | ||
import matplotlib.pyplot as plt | ||
import matplotlib.patheffects as patheffects | ||
|
||
# Direct to path | ||
fig, ax = plt.subplots(figsize=(6, 6)) | ||
path = Path.unit_circle() | ||
patch = patches.PathPatch(path, facecolor='none', lw=2, path_effects=[ | ||
patheffects.withTickedStroke(angle=-90, spacing=10, length=1)]) | ||
|
||
ax.add_patch(patch) | ||
ax.axis('equal') | ||
ax.set_xlim(-2, 2) | ||
ax.set_ylim(-2, 2) | ||
|
||
plt.show() | ||
|
||
############################################################################### | ||
# Lines and curves with plot and legend | ||
ramcdona marked this conversation as resolved.
Show resolved
Hide resolved
|
||
fig, ax = plt.subplots(figsize=(6, 6)) | ||
ax.plot([0, 1], [0, 1], label="Line", | ||
path_effects=[patheffects.withTickedStroke(spacing=7, angle=135)]) | ||
|
||
nx = 101 | ||
x = np.linspace(0.0, 1.0, nx) | ||
y = 0.3*np.sin(x*8) + 0.4 | ||
ax.plot(x, y, label="Curve", path_effects=[patheffects.withTickedStroke()]) | ||
|
||
ax.legend() | ||
|
||
plt.show() | ||
|
||
############################################################################### | ||
# Contour plot with objective and constraints. | ||
# Curves generated by contour to represent a typical constraint in an | ||
# optimization problem should be plotted with angles between zero and | ||
# 180 degrees. | ||
fig, ax = plt.subplots(figsize=(6, 6)) | ||
|
||
nx = 101 | ||
ny = 105 | ||
|
||
# Set up survey vectors | ||
xvec = np.linspace(0.001, 4.0, nx) | ||
yvec = np.linspace(0.001, 4.0, ny) | ||
|
||
# Set up survey matrices. Design disk loading and gear ratio. | ||
x1, x2 = np.meshgrid(xvec, yvec) | ||
|
||
# Evaluate some stuff to plot | ||
obj = x1**2 + x2**2 - 2*x1 - 2*x2 + 2 | ||
g1 = -(3*x1 + x2 - 5.5) | ||
g2 = -(x1 + 2*x2 - 4) | ||
g3 = 0.8 + x1**-3 - x2 | ||
|
||
cntr = ax.contour(x1, x2, obj, [0.01, 0.1, 0.5, 1, 2, 4, 8, 16], | ||
colors=('k',)) | ||
ax.clabel(cntr, fmt="%2.1f", use_clabeltext=True) | ||
|
||
cg1 = ax.contour(x1, x2, g1, [0], colors='black') | ||
plt.setp(cg1.collections, | ||
path_effects=[patheffects.withTickedStroke(angle=135)]) | ||
|
||
cg2 = ax.contour(x1, x2, g2, [0], colors='red') | ||
plt.setp(cg2.collections, | ||
path_effects=[patheffects.withTickedStroke(angle=60, length=2)]) | ||
|
||
cg3 = ax.contour(x1, x2, g3, [0], colors='blue') | ||
plt.setp(cg3.collections, | ||
path_effects=[patheffects.withTickedStroke(spacing=7)]) | ||
|
||
ax.set_xlim(0, 4) | ||
ax.set_ylim(0, 4) | ||
|
||
plt.show() |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,8 @@ | |
from matplotlib import colors as mcolors | ||
from matplotlib import patches as mpatches | ||
from matplotlib import transforms as mtransforms | ||
from matplotlib.path import Path | ||
import numpy as np | ||
|
||
|
||
class AbstractPathEffect: | ||
|
@@ -371,3 +373,148 @@ def draw_path(self, renderer, gc, tpath, affine, rgbFace): | |
if clip_path: | ||
self.patch.set_clip_path(*clip_path) | ||
self.patch.draw(renderer) | ||
|
||
|
||
class TickedStroke(AbstractPathEffect): | ||
""" | ||
A line-based PathEffect which draws a path with a ticked style. | ||
|
||
This line style is frequently used to represent constraints in | ||
optimization. The ticks may be used to indicate that one side | ||
of the line is invalid or to represent a closed boundary of a | ||
domain (i.e. a wall or the edge of a pipe). | ||
|
||
The spacing, length, and angle of ticks can be controlled. | ||
|
||
This line style is sometimes referred to as a hatched line. | ||
|
||
See also the :doc:`contour demo example | ||
</gallery/lines_bars_and_markers/lines_with_ticks_demo>`. | ||
|
||
See also the :doc:`contours in optimization example | ||
</gallery/images_contours_and_fields/contours_in_optimization_demo>`. | ||
""" | ||
|
||
def __init__(self, offset=(0, 0), | ||
spacing=10.0, angle=45.0, length=np.sqrt(2), | ||
**kwargs): | ||
""" | ||
Parameters | ||
---------- | ||
offset : pair of floats, default: (0, 0) | ||
The offset to apply to the path, in points. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are the two values? x/y? parallel/perpendicular? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a verbatim copy of the documentation for this parameter inherited from AbstractPathEffect. See line 30, 301, 360 of this file. It is paraphrased on line 233. It is an offset in x, y. |
||
spacing : float, default: 10.0 | ||
The spacing between ticks in points. | ||
angle : float, default: 45.0 | ||
The angle between the path and the tick in degrees. The angle | ||
is measured as if you were an ant walking along the curve, with | ||
zero degrees pointing directly ahead, 90 to your left, -90 | ||
to your right, and 180 behind you. | ||
length : float, default: 1.414 | ||
The length of the tick relative to spacing. | ||
Recommended length = 1.414 (sqrt(2)) when angle=45, length=1.0 | ||
when angle=90 and length=2.0 when angle=60. | ||
**kwargs | ||
Extra keywords are stored and passed through to | ||
:meth:`AbstractPathEffect._update_gc`. | ||
|
||
Examples | ||
-------- | ||
See :doc:`/gallery/misc/tickedstroke_demo`. | ||
""" | ||
super().__init__(offset) | ||
|
||
self._spacing = spacing | ||
self._angle = angle | ||
self._length = length | ||
self._gc = kwargs | ||
ramcdona marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
def draw_path(self, renderer, gc, tpath, affine, rgbFace): | ||
""" | ||
Draw the path with updated gc. | ||
""" | ||
# Do not modify the input! Use copy instead. | ||
gc0 = renderer.new_gc() | ||
gc0.copy_properties(gc) | ||
|
||
gc0 = self._update_gc(gc0, self._gc) | ||
trans = affine + self._offset_transform(renderer) | ||
|
||
theta = -np.radians(self._angle) | ||
trans_matrix = np.array([[np.cos(theta), -np.sin(theta)], | ||
[np.sin(theta), np.cos(theta)]]) | ||
|
||
# Convert spacing parameter to pixels. | ||
spcpx = renderer.points_to_pixels(self._spacing) | ||
|
||
# Transform before evaluation because to_polygons works at resolution | ||
# of one -- assuming it is working in pixel space. | ||
transpath = affine.transform_path(tpath) | ||
|
||
# Evaluate path to straight line segments that can be used to | ||
# construct line ticks. | ||
polys = transpath.to_polygons(closed_only=False) | ||
|
||
for p in polys: | ||
x = p[:, 0] | ||
y = p[:, 1] | ||
|
||
# Can not interpolate points or draw line if only one point in | ||
# polyline. | ||
if x.size < 2: | ||
continue | ||
|
||
# Find distance between points on the line | ||
ds = np.hypot(x[1:] - x[:-1], y[1:] - y[:-1]) | ||
|
||
# Build parametric coordinate along curve | ||
s = np.concatenate(([0.0], np.cumsum(ds))) | ||
stot = s[-1] | ||
|
||
num = int(np.ceil(stot / spcpx))-1 | ||
# Pick parameter values for ticks. | ||
s_tick = np.linspace(spcpx/2, stot-spcpx/2, num) | ||
ramcdona marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
# Find points along the parameterized curve | ||
x_tick = np.interp(s_tick, s, x) | ||
y_tick = np.interp(s_tick, s, y) | ||
|
||
# Find unit vectors in local direction of curve | ||
delta_s = self._spacing * .001 | ||
u = (np.interp(s_tick + delta_s, s, x) - x_tick) / delta_s | ||
v = (np.interp(s_tick + delta_s, s, y) - y_tick) / delta_s | ||
|
||
# Normalize slope into unit slope vector. | ||
n = np.hypot(u, v) | ||
mask = n == 0 | ||
n[mask] = 1.0 | ||
|
||
uv = np.array([u / n, v / n]).T | ||
uv[mask] = np.array([0, 0]).T | ||
|
||
# Rotate and scale unit vector into tick vector | ||
dxy = np.dot(uv, trans_matrix) * self._length * spcpx | ||
|
||
# Build tick endpoints | ||
x_end = x_tick + dxy[:, 0] | ||
y_end = y_tick + dxy[:, 1] | ||
|
||
# Interleave ticks to form Path vertices | ||
xyt = np.empty((2 * num, 2), dtype=x_tick.dtype) | ||
xyt[0::2, 0] = x_tick | ||
xyt[1::2, 0] = x_end | ||
xyt[0::2, 1] = y_tick | ||
xyt[1::2, 1] = y_end | ||
|
||
# Build up vector of Path codes | ||
codes = np.tile([Path.MOVETO, Path.LINETO], num) | ||
|
||
# Construct and draw resulting path | ||
h = Path(xyt, codes) | ||
# Transform back to data space during render | ||
renderer.draw_path(gc0, h, affine.inverted() + trans, rgbFace) | ||
|
||
gc0.restore() | ||
|
||
|
||
withTickedStroke = _subclass_with_normal(effect_class=TickedStroke) |
Uh oh!
There was an error while loading. Please reload this page.