Patch fully covered with Matplotlib
Question:
I’m trying to make a program that covers the area of a function based on a figure. My main problem is that the pattern leaves a lot of empty spaces. One way that I could think of to fix this was by trying to indicate in the code that each figure must touch at least 2 vertices of another figure. However, I have not been able to implement it successfully.
I noticed the "numVertices" parameter in the documentation. However, I didn’t get anything so far. Could someone help me with this?
Code:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle, RegularPolygon
from scipy.integrate import quad
def plot_function_with_pattern(func, x_range, y_range, base_size, shape="rectangle", include_touching=True, start_x=None, start_y=None):
fig, ax = plt.subplots()
x = np.linspace(x_range[0], x_range[1], 1000)
y = func(x)
ax.plot(x, y)
# Calculate number of figures that fit inside function
num_figures = 0
if start_x is None:
start_x = x_range[0]
if start_y is None:
start_y = y_range[0]
for x_i in np.arange(start_x, x_range[1], base_size):
for y_i in np.arange(start_y, y_range[1], base_size):
if func(x_i) >= y_i:
# Check if the figure is touching the function
touching = func(x_i + base_size/2) >= y_i or func(x_i + base_size/2) >= y_i + base_size or func(x_i) >= y_i + base_size/2 or func(x_i + base_size) >= y_i + base_size/2
if include_touching or not touching:
# Create shape patch
if shape == "rectangle":
facecolor = 'red' if not include_touching else 'green'
patch = Rectangle((x_i, y_i), base_size, radius=base_size, facecolor=facecolor)
elif shape == "triangle":
facecolor = 'red' if not include_touching else 'green'
patch = RegularPolygon((x_i+base_size/2, y_i), 3, radius=base_size/np.sqrt(3), orientation=0, facecolor=facecolor)
elif shape == "square":
facecolor = 'red' if not include_touching else 'green'
patch = RegularPolygon((x_i+base_size/2, y_i+base_size/2), 4, radius=base_size/np.sqrt(2), orientation=np.pi/4, facecolor=facecolor)
elif shape == "hexagon":
facecolor = 'red' if not include_touching else 'green'
patch = RegularPolygon((x_i+base_size/2, y_i), 6, radius=base_size/np.sqrt(3), orientation=np.pi/2, facecolor=facecolor)
ax.add_patch(patch)
num_figures += 1
plt.show()
# Calculate integral of the function
integral, _ = quad(func, x_range[0], x_range[1])
# Subtract the amount of figures touching the function if include_touching is False
if not include_touching:
num_figures -= int(integral / base_size**2)
total = num_figures*-1
print(f"{total} {shape}(s) are inside the function and NOT TOUCHING the function. The base size was {base_size} unit(s) with x range of: {x_range} and y range of: {y_range}. The area of the function is {integral:.4f}.")
elif include_touching:
print(f"{num_figures} {shape}(s) are inside the function and TOUCHING the function. The base size was {base_size} unit(s) with x range of: {x_range} and y range of: {y_range}. The area of the function is {integral:.4f}.")
# Example usage
def func(x):
return -0.00002*x**6 + 0.0011*x**5 -0.024 *x**4 + 0.24*x**3 -0.8*x**2 -x + 10
plot_function_with_pattern(func, x_range=(0, 19.308), y_range=(0, 17.55), base_size=1, shape="hexagon", include_touching=True)
Answers:
It took me a while to understand what you are trying to achieve but I think I got it.
First you should specify a color for patch edges so that the shapes are visible (the squares are not visible in your example). Then since your ‘rectangle’ case is incomplete (missing height) I would simply handle every shape as a RegularPolygon
. Now if you want to fill the blanks between shapes, you have to move/rotate your polygons every two columns (only squares can be put close to each other without blanks). You can specify these transformations in a dictionary (I simplified some arguments for better readability/generalization):
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle, RegularPolygon
from scipy.integrate import quad
def plot_function_with_pattern(func, x_range, y_range, base_size=1, num_vertices=4, include_touching=True, face_col='green', edge_col='grey'):
shape_dict = {
3: { # triangle
'radius': base_size/np.sqrt(3),
'orientation': 0,
'rotation': np.pi, # rotation every two columns
'y_offset': base_size/(2*np.sqrt(3)), # y_offset every two columns
'base_step_x': base_size/2,
'base_step_y': np.sqrt(3)*base_size/2
},
4: { # square
'radius': base_size/np.sqrt(2),
'orientation': np.pi/4
},
6: { # hexagon
'radius': base_size/2,
'orientation': np.pi/2,
'y_offset': np.sqrt(3)*base_size/4, # y_offset every two columns
'base_step_x': 3*base_size/4,
'base_step_y': np.sqrt(3)*base_size/2
}
}
if not num_vertices in shape_dict:
print(f"Error: polygon with {num_vertices} vertices are not supported.")
return
fig, ax = plt.subplots()
x = np.linspace(x_range[0], x_range[1], 1000)
y = func(x)
ax.plot(x, y)
num_figures = 0
base_step_x = shape_dict[num_vertices].get('base_step_x', base_size)
base_step_y = shape_dict[num_vertices].get('base_step_y', base_size)
radius = shape_dict[num_vertices]['radius']
orientation = shape_dict[num_vertices]['orientation']
y_start = shape_dict[num_vertices].get('y_offset', base_size/2) # align height of the first shape base with 0
for i, x_i in enumerate(np.arange(x_range[0], x_range[1], base_step_x)):
y_offset = y_start
orient = orientation
if (i % 2 != 0):
y_offset += shape_dict[num_vertices].get('y_offset', 0)
orient += shape_dict[num_vertices].get('rotation', 0)
for y_i in np.arange(y_range[0] + y_offset, y_range[1] + y_offset, base_step_y):
patch = RegularPolygon((x_i, y_i), num_vertices, radius=radius, orientation=orient, fc=face_col, ec=edge_col)
if patch.contains_point((x_i, func(x_i)), radius=1e-9): # omitting radius triggers a mpl bug
if include_touching:
ax.add_patch(patch)
num_figures += 1
break
else:
ax.add_patch(patch)
num_figures += 1
# Calculate integral of the function
integral, _ = quad(func, x_range[0], x_range[1])
# Subtract the amount of figures touching the function if include_touching is False
print(f"{num_figures} polygons with {num_vertices} vertices are inside the function or touching the function. The base size was {base_size} unit(s) with x range of: {x_range} and y range of: {y_range}. The area of the function is {integral:.4f}.")
plt.show()
# Example usage
def func(x):
return -0.00002*x**6 + 0.0011*x**5 -0.024 *x**4 + 0.24*x**3 -0.8*x**2 -x + 10
plot_function_with_pattern(func, x_range=(0, 19.308), y_range=(0, 17.55), base_size=0.8, include_touching=True, num_vertices=6)
Edit: offsets, steps and rotations are calculated using trigonometry see this page for instance
Triangle:
Square:
Hexagon:
I’m trying to make a program that covers the area of a function based on a figure. My main problem is that the pattern leaves a lot of empty spaces. One way that I could think of to fix this was by trying to indicate in the code that each figure must touch at least 2 vertices of another figure. However, I have not been able to implement it successfully.
I noticed the "numVertices" parameter in the documentation. However, I didn’t get anything so far. Could someone help me with this?
Code:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle, RegularPolygon
from scipy.integrate import quad
def plot_function_with_pattern(func, x_range, y_range, base_size, shape="rectangle", include_touching=True, start_x=None, start_y=None):
fig, ax = plt.subplots()
x = np.linspace(x_range[0], x_range[1], 1000)
y = func(x)
ax.plot(x, y)
# Calculate number of figures that fit inside function
num_figures = 0
if start_x is None:
start_x = x_range[0]
if start_y is None:
start_y = y_range[0]
for x_i in np.arange(start_x, x_range[1], base_size):
for y_i in np.arange(start_y, y_range[1], base_size):
if func(x_i) >= y_i:
# Check if the figure is touching the function
touching = func(x_i + base_size/2) >= y_i or func(x_i + base_size/2) >= y_i + base_size or func(x_i) >= y_i + base_size/2 or func(x_i + base_size) >= y_i + base_size/2
if include_touching or not touching:
# Create shape patch
if shape == "rectangle":
facecolor = 'red' if not include_touching else 'green'
patch = Rectangle((x_i, y_i), base_size, radius=base_size, facecolor=facecolor)
elif shape == "triangle":
facecolor = 'red' if not include_touching else 'green'
patch = RegularPolygon((x_i+base_size/2, y_i), 3, radius=base_size/np.sqrt(3), orientation=0, facecolor=facecolor)
elif shape == "square":
facecolor = 'red' if not include_touching else 'green'
patch = RegularPolygon((x_i+base_size/2, y_i+base_size/2), 4, radius=base_size/np.sqrt(2), orientation=np.pi/4, facecolor=facecolor)
elif shape == "hexagon":
facecolor = 'red' if not include_touching else 'green'
patch = RegularPolygon((x_i+base_size/2, y_i), 6, radius=base_size/np.sqrt(3), orientation=np.pi/2, facecolor=facecolor)
ax.add_patch(patch)
num_figures += 1
plt.show()
# Calculate integral of the function
integral, _ = quad(func, x_range[0], x_range[1])
# Subtract the amount of figures touching the function if include_touching is False
if not include_touching:
num_figures -= int(integral / base_size**2)
total = num_figures*-1
print(f"{total} {shape}(s) are inside the function and NOT TOUCHING the function. The base size was {base_size} unit(s) with x range of: {x_range} and y range of: {y_range}. The area of the function is {integral:.4f}.")
elif include_touching:
print(f"{num_figures} {shape}(s) are inside the function and TOUCHING the function. The base size was {base_size} unit(s) with x range of: {x_range} and y range of: {y_range}. The area of the function is {integral:.4f}.")
# Example usage
def func(x):
return -0.00002*x**6 + 0.0011*x**5 -0.024 *x**4 + 0.24*x**3 -0.8*x**2 -x + 10
plot_function_with_pattern(func, x_range=(0, 19.308), y_range=(0, 17.55), base_size=1, shape="hexagon", include_touching=True)
It took me a while to understand what you are trying to achieve but I think I got it.
First you should specify a color for patch edges so that the shapes are visible (the squares are not visible in your example). Then since your ‘rectangle’ case is incomplete (missing height) I would simply handle every shape as a RegularPolygon
. Now if you want to fill the blanks between shapes, you have to move/rotate your polygons every two columns (only squares can be put close to each other without blanks). You can specify these transformations in a dictionary (I simplified some arguments for better readability/generalization):
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.patches import Rectangle, RegularPolygon
from scipy.integrate import quad
def plot_function_with_pattern(func, x_range, y_range, base_size=1, num_vertices=4, include_touching=True, face_col='green', edge_col='grey'):
shape_dict = {
3: { # triangle
'radius': base_size/np.sqrt(3),
'orientation': 0,
'rotation': np.pi, # rotation every two columns
'y_offset': base_size/(2*np.sqrt(3)), # y_offset every two columns
'base_step_x': base_size/2,
'base_step_y': np.sqrt(3)*base_size/2
},
4: { # square
'radius': base_size/np.sqrt(2),
'orientation': np.pi/4
},
6: { # hexagon
'radius': base_size/2,
'orientation': np.pi/2,
'y_offset': np.sqrt(3)*base_size/4, # y_offset every two columns
'base_step_x': 3*base_size/4,
'base_step_y': np.sqrt(3)*base_size/2
}
}
if not num_vertices in shape_dict:
print(f"Error: polygon with {num_vertices} vertices are not supported.")
return
fig, ax = plt.subplots()
x = np.linspace(x_range[0], x_range[1], 1000)
y = func(x)
ax.plot(x, y)
num_figures = 0
base_step_x = shape_dict[num_vertices].get('base_step_x', base_size)
base_step_y = shape_dict[num_vertices].get('base_step_y', base_size)
radius = shape_dict[num_vertices]['radius']
orientation = shape_dict[num_vertices]['orientation']
y_start = shape_dict[num_vertices].get('y_offset', base_size/2) # align height of the first shape base with 0
for i, x_i in enumerate(np.arange(x_range[0], x_range[1], base_step_x)):
y_offset = y_start
orient = orientation
if (i % 2 != 0):
y_offset += shape_dict[num_vertices].get('y_offset', 0)
orient += shape_dict[num_vertices].get('rotation', 0)
for y_i in np.arange(y_range[0] + y_offset, y_range[1] + y_offset, base_step_y):
patch = RegularPolygon((x_i, y_i), num_vertices, radius=radius, orientation=orient, fc=face_col, ec=edge_col)
if patch.contains_point((x_i, func(x_i)), radius=1e-9): # omitting radius triggers a mpl bug
if include_touching:
ax.add_patch(patch)
num_figures += 1
break
else:
ax.add_patch(patch)
num_figures += 1
# Calculate integral of the function
integral, _ = quad(func, x_range[0], x_range[1])
# Subtract the amount of figures touching the function if include_touching is False
print(f"{num_figures} polygons with {num_vertices} vertices are inside the function or touching the function. The base size was {base_size} unit(s) with x range of: {x_range} and y range of: {y_range}. The area of the function is {integral:.4f}.")
plt.show()
# Example usage
def func(x):
return -0.00002*x**6 + 0.0011*x**5 -0.024 *x**4 + 0.24*x**3 -0.8*x**2 -x + 10
plot_function_with_pattern(func, x_range=(0, 19.308), y_range=(0, 17.55), base_size=0.8, include_touching=True, num_vertices=6)
Edit: offsets, steps and rotations are calculated using trigonometry see this page for instance
Triangle:
Square:
Hexagon: