Skip to content

Sketch seed #26050

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

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
13 changes: 13 additions & 0 deletions doc/api/next_api_changes/behavior/26050-AS.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
Seed for ``path.sketch`` will have a rolling (auto incrementing) behaviour
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

The seed for the internal Pseudo number generator will now have an auto changing behavior.
This means that the C code of every artist will get a different seed every time it is called
and this will be done in a deterministic manner.

Two figures sketched with the same parameters and different seed will look different from one another.

``Artist.get_sketch_params()`` will now return a 4-tuple instead of a 3-tuple consisting of
(scale, length, randomness, seed) of the form (float, float, float, int).

See 'What's new' on how to set a value to the seed and its behaviour.
67 changes: 67 additions & 0 deletions doc/users/next_whats_new/sketch_seed.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
``sketch_seed`` parameter for rcParams
--------------------------------------

`~matplotlib.rcParams` now has a new parameter ``path.sketch_seed``.
Its default value is 0 and accepted values are any non negative integer.
This allows the user to set the seed for the internal pseudo random number generator in one of three ways.

1) Directly changing the rcParam:

rcParams['path.sketch_seed'] = 20

2) Passing a value to the new *seed* parameter of `~matplotlib.pyplot.xkcd` function:

plt.xkcd(seed=20)

3) Passing a value to the new *seed* parameter of matplotlib.artist.set_sketch_params function:

ln = plt.plot(x, y)
ln[0].set_sketch_params(seed = 20)

The seed will also have a changing characteristic for every artist which will be done in a deterministic manner.


.. plot::
:include-source: true

import matplotlib.pyplot as plt
from matplotlib import rcParams

with plt.xkcd():
rcParams['path.sketch_seed']=0
rcParams['path.sketch']=(2,120,40)
pat,txt=plt.pie([10,20,30,40],wedgeprops={'edgecolor':'black'})
plt.legend(pat,['first','second','third','fourth'],loc='best')
plt.title("seed = 0")
plt.show()

.. plot::
:include-source: true

import matplotlib.pyplot as plt
from matplotlib import rcParams

fig, ax = plt.subplots()
x = np.linspace(0.7, 1.42, 100)
y = x ** 2
ln = ax.plot(x, y, color='black')
ln[0].set_sketch_params(100, 100, 20, 40)
plt.title("seed = 40")
plt.show()

.. plot::
:include-source: true

import matplotlib.pyplot as plt
from matplotlib import rcParams

with plt.xkcd(seed=19680801):
import matplotlib
from matplotlib import gridspec

rcParams['path.sketch']=(2,120,40)

pat,txt=plt.pie([10,20,30,40],wedgeprops={'edgecolor':'black'})
plt.legend(pat,['first','second','third','fourth'],loc='best')
plt.title("seed = 19680801")
plt.show()
17 changes: 14 additions & 3 deletions lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ def __init__(self):
self._gid = None
self._snap = None
self._sketch = mpl.rcParams['path.sketch']
self._sketch_seed = mpl.rcParams['path.sketch_seed']
self._path_effects = mpl.rcParams['path.effects']
self._sticky_edges = _XYPair([], [])
self._in_layout = True
Expand Down Expand Up @@ -681,7 +682,8 @@ def get_sketch_params(self):
"""
return self._sketch

def set_sketch_params(self, scale=None, length=None, randomness=None):
def set_sketch_params(self, scale=None, length=None, randomness=None,
seed=None):
"""
Set the sketch parameters.

Expand All @@ -701,12 +703,21 @@ def set_sketch_params(self, scale=None, length=None, randomness=None):
The PGF backend uses this argument as an RNG seed and not as
described above. Using the same seed yields the same random shape.

.. ACCEPTS: (scale: float, length: float, randomness: float)
seed : int, optional
Seed for the internal pseudo-random number generator.

.. versionadded:: 3.8

.. ACCEPTS: (scale: float, length: float, randomness: float, seed: int)
"""
if seed is not None:
self._sketch_seed = seed

if scale is None:
self._sketch = None
else:
self._sketch = (scale, length or 128.0, randomness or 16.0)
self._sketch = (scale, length or 128.0, randomness or 16.0,
self._sketch_seed)
self.stale = True

def set_path_effects(self, path_effects):
Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/artist.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,13 @@ class Artist:
def set_gid(self, gid: str | None) -> None: ...
def get_snap(self) -> bool | None: ...
def set_snap(self, snap: bool | None) -> None: ...
def get_sketch_params(self) -> tuple[float, float, float] | None: ...
def get_sketch_params(self) -> tuple[float, float, float, int] | None: ...
def set_sketch_params(
self,
scale: float | None = ...,
length: float | None = ...,
randomness: float | None = ...,
seed: int | None = ...,
) -> None: ...
def set_path_effects(self, path_effects: list[AbstractPathEffect]) -> None: ...
def get_path_effects(self) -> list[AbstractPathEffect]: ...
Expand Down
27 changes: 25 additions & 2 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,19 @@ def _draw_disabled(self):

return _setattr_cm(self, **no_ops)

@property
def _seed_increment(self):
"""
seed increment for renderer.
It is used to implement the rolling characteristic for seed
"""
self.__seed_increment += 1
return self.__seed_increment

@_seed_increment.setter
def _seed_increment(self, value):
self.__seed_increment = value


class GraphicsContextBase:
"""An abstract base class that provides color, line styles, etc."""
Expand Down Expand Up @@ -1062,7 +1075,8 @@ def get_sketch_params(self):
"""
return self._sketch

def set_sketch_params(self, scale=None, length=None, randomness=None):
def set_sketch_params(self, scale=None, length=None, randomness=None,
seed=None):
"""
Set the sketch parameters.

Expand All @@ -1076,10 +1090,19 @@ def set_sketch_params(self, scale=None, length=None, randomness=None):
The length of the wiggle along the line, in pixels.
randomness : float, default: 16
The scale factor by which the length is shrunken or expanded.
seed : int, optional
Seed for the internal pseudo-random number generator.

.. versionadded:: 3.8
"""

self._sketch = (
None if scale is None
else (scale, length or 128., randomness or 16.))
else (scale,
length or rcParams['path.sketch'][1],
randomness or rcParams['path.sketch'][2],
seed or rcParams['path.sketch_seed'])
)


class TimerBase:
Expand Down
5 changes: 5 additions & 0 deletions lib/matplotlib/backend_bases.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ class RendererBase:
def stop_rasterizing(self) -> None: ...
def start_filter(self) -> None: ...
def stop_filter(self, filter_func) -> None: ...
@property
def _seed_increment(self) -> int: ...
@_seed_increment.setter
def _seed_increment(self, value: int) -> None: ...

class GraphicsContextBase:
def __init__(self) -> None: ...
Expand Down Expand Up @@ -180,6 +184,7 @@ class GraphicsContextBase:
scale: float | None = ...,
length: float | None = ...,
randomness: float | None = ...,
seed:int | None = ...,
) -> None: ...

class TimerBase:
Expand Down
2 changes: 1 addition & 1 deletion lib/matplotlib/backends/backend_pgf.py
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ def _print_pgf_path(self, gc, path, transform, rgbFace=None):
# and has a separate "scale" argument for the amplitude.
# -> Use "randomness" as PRNG seed to allow the user to force the
# same shape on multiple sketched lines
scale, length, randomness = sketch_params
scale, length, randomness, seed = sketch_params
if scale is not None:
# make matplotlib and PGF rendering visually similar
length *= 0.5
Expand Down
2 changes: 2 additions & 0 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
Artist, allow_rasterization, _finalize_rasterization)
from matplotlib.backend_bases import (
DrawEvent, FigureCanvasBase, NonGuiException, MouseButton, _get_renderer)

import matplotlib._api as _api
import matplotlib.cbook as cbook
import matplotlib.colorbar as cbar
Expand Down Expand Up @@ -3141,6 +3142,7 @@ def draw(self, renderer):

artists = self._get_draw_artists(renderer)
try:
renderer._seed_increment = 0
renderer.open_group('figure', gid=self.get_gid())
if self.axes and self.get_layout_engine() is not None:
try:
Expand Down
3 changes: 2 additions & 1 deletion lib/matplotlib/lines.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,7 +823,8 @@ def draw(self, renderer):
gc.set_foreground(ec_rgba, isRGBA=True)
if self.get_sketch_params() is not None:
scale, length, randomness = self.get_sketch_params()
gc.set_sketch_params(scale/2, length/2, 2*randomness)
seed = self._sketch_seed
gc.set_sketch_params(scale/2, length/2, 2*randomness, seed)
Copy link
Member

Choose a reason for hiding this comment

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

since this isn't incremented, are you sure that the rolling is correctly implemented?
like xkcd(seed=1234)
ax1.plot([1,2,3])
ax2.plot([1,2,3])

should have lines that look different - and yes please test if you haven't


marker = self._marker

Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/mpl-data/matplotlibrc
Original file line number Diff line number Diff line change
Expand Up @@ -677,6 +677,7 @@
# - *randomness* is the factor by which the length is
# randomly scaled.
#path.effects:
#path.sketch_seed: 0 # seed for the internal pseudo number generator.


## ***************************************************************************
Expand Down
4 changes: 3 additions & 1 deletion lib/matplotlib/patches.py
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,9 @@ def _draw_paths_with_artist_properties(
gc.set_hatch_color(self._hatch_color)

if self.get_sketch_params() is not None:
gc.set_sketch_params(*self.get_sketch_params())
scale, length, randomness = self.get_sketch_params()
gc.set_sketch_params(scale, length, randomness,
self._sketch_seed+renderer._seed_increment)

if self.get_path_effects():
from matplotlib.patheffects import PathEffectRenderer
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/path.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,8 +382,8 @@ def iter_segments(self, transform=None, remove_nans=True, clip=None,
If True, curve segments will be returned as curve segments.
If False, all curves will be converted to line segments.
sketch : None or sequence, optional
If not None, must be a 3-tuple of the form
(scale, length, randomness), representing the sketch parameters.
If not None, must be a 4-tuple of the form
(scale, length, randomness, seed), representing the sketch parameters.
"""
if not len(self):
return
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/path.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ class Path:
stroke_width: float = ...,
simplify: bool | None = ...,
curves: bool = ...,
sketch: tuple[float, float, float] | None = ...,
sketch: tuple[float, float, float, int] | None = ...,
) -> Generator[tuple[np.ndarray, np.uint8], None, None]: ...
def iter_bezier(self, **kwargs) -> Generator[BezierSegment, None, None]: ...
def cleaned(
Expand All @@ -74,7 +74,7 @@ class Path:
curves: bool = ...,
stroke_width: float = ...,
snap: bool | None = ...,
sketch: tuple[float, float, float] | None = ...
sketch: tuple[float, float, float, int] | None = ...
) -> Path: ...
def transformed(self, transform: Transform) -> Path: ...
def contains_point(
Expand Down
9 changes: 7 additions & 2 deletions lib/matplotlib/pyplot.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,8 +701,8 @@ def setp(obj, *args, **kwargs):


def xkcd(
scale: float = 1, length: float = 100, randomness: float = 2
) -> ExitStack:
scale: float = 1, length: float = 100, randomness: float = 2,
seed: int | None = None) -> ExitStack:
"""
Turn on `xkcd <https://xkcd.com/>`_ sketch-style drawing mode. This will
only have effect on things drawn after this function is called.
Expand All @@ -718,6 +718,8 @@ def xkcd(
The length of the wiggle along the line.
randomness : float, optional
The scale factor by which the length is shrunken or expanded.
seed: int, optional
Seed for the internal pseudo-random number generator.

Notes
-----
Expand All @@ -738,6 +740,9 @@ def xkcd(
# This cannot be implemented in terms of contextmanager() or rc_context()
# because this needs to work as a non-contextmanager too.

if seed is not None:
rcParams['path.sketch_seed'] = seed

if rcParams['text.usetex']:
raise RuntimeError(
"xkcd mode is not compatible with text.usetex = True")
Expand Down
10 changes: 10 additions & 0 deletions lib/matplotlib/rcsetup.py
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,15 @@ def validate_sketch(s):
raise ValueError("Expected a (scale, length, randomness) triplet")


def validate_sketch_seed(s):
s = validate_int(s)

if s >= 0:
return s
else:
raise ValueError("seed must be a non negative integer")


def _validate_greaterthan_minushalf(s):
s = validate_float(s)
if s > -0.5:
Expand Down Expand Up @@ -1288,6 +1297,7 @@ def _convert_validator_spec(key, conv):
"path.simplify_threshold": _validate_greaterequal0_lessequal1,
"path.snap": validate_bool,
"path.sketch": validate_sketch,
"path.sketch_seed": validate_sketch_seed,
"path.effects": validate_anylist,
"agg.path.chunksize": validate_int, # 0 to disable chunking

Expand Down
1 change: 1 addition & 0 deletions lib/matplotlib/rcsetup.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def _validate_linestyle(s: Any) -> LineStyleType: ...
def validate_markeverylist(s: Any) -> list[MarkEveryType]: ...
def validate_bbox(s: Any) -> Literal["tight", "standard"] | None: ...
def validate_sketch(s: Any) -> None | tuple[float, float, float]: ...
def validate_sketch_seed(s: Any) -> int: ...
def validate_hatch(s: Any) -> str: ...
def validate_hatchlist(s: Any) -> list[str]: ...
def validate_dashlist(s: Any) -> list[list[float]]: ...
Expand Down
Binary file modified lib/matplotlib/tests/baseline_images/test_path/xkcd.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified lib/matplotlib/tests/baseline_images/test_path/xkcd_marker.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion lib/matplotlib/tests/test_backend_pgf.py
Original file line number Diff line number Diff line change
Expand Up @@ -384,7 +384,7 @@ def test_sketch_params():
ax.set_yticks([])
ax.set_frame_on(False)
handle, = ax.plot([0, 1])
handle.set_sketch_params(scale=5, length=30, randomness=42)
handle.set_sketch_params(scale=5, length=30, randomness=42, seed=0)

with BytesIO() as fd:
fig.savefig(fd, format='pgf')
Expand Down
Loading