Thanks to visit codestin.com
Credit goes to github.com

Skip to content

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

Merged
merged 1 commit into from
Aug 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
29 changes: 29 additions & 0 deletions examples/lines_bars_and_markers/lines_with_ticks_demo.py
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()
99 changes: 99 additions & 0 deletions examples/misc/tickedstroke_demo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
"""
=======================
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
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()
147 changes: 147 additions & 0 deletions lib/matplotlib/patheffects.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the two values? x/y? parallel/perpendicular?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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

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)

# 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)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading