# Imports
import os
import numpy as np
import colorsys
# Matplotlib imports
from matplotlib import pyplot as plt
# Scipy imports
from scipy.spatial.distance import cdist
from scipy.interpolate import interp1d
[docs]
def chsave(name="figure",dir_path="images",pdf=True,img_dpi=300,pdf_dpi=200):
"""
Saves the current matplotlib figure as PNG and optionally PDF with customizable *dpi*
(dots per inch).
Parameters
name : :class:`str`
The name of the figure. The image is saved as "dir_path/name.png" (default: "figure").
Other Parameters
dir_path : :class:`str`
Path to the directory where the image is saved. If it doesn't exist, it will be created
(default: "images")
pdf : :class:`bool`
Whether to also save the image as a PDF.
img_dpi : :class:`int`
The *dpi* of the PNG image.
pdf_dpi : :class:`int`
The *dpi* of the PDF image.
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: mock-block
import cachai.utilities as chu
plt.figure(figsize=(10,10))
plt.scatter([1,2,3],[1,4,9],color="magenta")
chu.chsave("my_plot")
"""
if not os.path.exists(dir_path): os.makedirs(dir_path)
plt.savefig(os.path.join(dir_path,f'{name}.png'),
bbox_inches='tight',pad_inches=0.3,dpi=img_dpi)
if pdf:
if not os.path.exists(os.path.join(dir_path,'pdf')):
os.makedirs(os.path.join(dir_path,'pdf'))
plt.savefig(os.path.join(dir_path,'pdf',f'{name}.pdf'),
bbox_inches='tight',pad_inches=0.3,dpi=pdf_dpi)
[docs]
def angdist(alpha,beta):
"""
Calculates the minimal angular distance between two angles in radians.
Parameters
alpha : :class:`float`
Angle in radians.
beta : :class:`float`
Angle in radians.
Returns
:class:`float`
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
import numpy as np
A = np.pi/4
B = 3*np.pi/4
distance = chu.angdist(A,B)
print("Distance in radians:",distance)
print("Distance in degrees:",np.rad2deg(distance))
.. code-block:: text
:class: out-block
Distance in radians: 1.5707963267948966
Distance in degrees: 90.0
"""
diff = np.abs(alpha - beta) % (2 * np.pi)
return np.min([diff, 2 * np.pi - diff])
def _angspace(alpha,beta,n=200):
""":meta-private:
Generate a linear space of angles
"""
theta = abs(beta-alpha)
ndots = int(theta*n/(2*np.pi))
if ndots == 1: ndots = 2
return np.linspace(alpha,beta,ndots)
[docs]
def map_from_curve(curve=None,xlim=(-1,1),ylim=(-1,1),resolution=200):
"""
Generates a map (2D matrix) where each point value is based on its proximity to the nearest
point along a specified curve.
Parameters
curve : :class:`numpy.ndarray`
1D array of points (x,y) defining the reference curve.
xlim : :class:`tuple` or :class:`array-like`, optional
x-axis boundaries of the map.
ylim : :class:`tuple` or :class:`array-like`, optional
y-axis boundaries of the map.
resolution : :class:`int`, optional
Number of grid points along each axis for the output map.
Returns
:class:`numpy.ndarray` : 2D array
or
:class:`None` : If ``curve = None``
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
import numpy as np
# Constructing a sine curve
x_array = np.linspace(0,2*np.pi,500)
y_array = np.sin(x_array)
curve = np.array([(x,y) for x,y in zip(x_array,y_array)])
my_map = chu.map_from_curve(curve,xlim=(0,2*np.pi),ylim=(-1,1),resolution=5)
print(my_map)
.. code-block:: text
:class: out-block
[[-1. -0.10220441 0.15430862 0.498998 0.84769539]
[-1. -0.77955912 0.07815631 0.498998 0.91983968]
[-1. -0.498998 0.00200401 0.498998 1. ]
[-0.91983968 -0.498998 -0.07815631 0.77955912 1. ]
[-0.84769539 -0.498998 -0.15430862 0.10220441 1. ]]
"""
if curve is None: return None
# Values from curve
values = np.linspace(-1,1,len(curve))
# Mesh
x = np.linspace(*xlim, resolution)
y = np.linspace(*ylim, resolution)
grid_x, grid_y = np.meshgrid(x, y, indexing='xy')
grid_points = np.column_stack((grid_x.ravel(), grid_y.ravel()))
# Obtain the nearest point in the curve and use that value
distances = cdist(grid_points, curve)
nearest_point_indexes = np.argmin(distances, axis=1)
map_flat = values[nearest_point_indexes]
map_matrix = map_flat.reshape(resolution, resolution)
return map_matrix
[docs]
def colormapped_patch(patch,map_matrix,ax=None,colormap="coolwarm",
zorder=5,alpha=0.5,rasterized=False):
"""
Applies a colormap to a patch object using a precomputed map matrix, creating a color-filled
shape.
Parameters
patch : :class:`matplotlib.patches.Patch` and similar
Matplotlib patch object to be filled with colors.
map_matrix : :class:`numpy.ndarray`
2D array containing color values for the mapping.
Returns
:class:`matplotlib.image.AxesImage`
Other Parameters
ax : :class:`matplotlib.axes.Axes`
Axes object where the patch will be drawn (default: current axes).
colormap : :class:`str` or :class:`matplotlib.colors.LinearSegmentedColormap`
Matplotlib colormap to use.
zorder : :class:`int`
Rendering order layer for the patch.
alpha : :class:`float`
Transparency level of the filled patch.
rasterized : :class:`bool`
Whether to rasterize the patch for better performance.
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: mock-block
import cachai.utilities as chu
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.patches import Circle
# Create a circular patch
circle = Circle((0.5, 0.5), 0.3)
# Generate a simple gradient map
gradient = np.linspace(-1, 1, 400).reshape(20, 20)
fig, ax = plt.subplots()
ax.add_patch(circle)
chu.colormapped_patch(circle, gradient, ax=ax)
plt.show()
"""
if ax is None: ax = plt.gca()
vertices = patch.get_path().vertices
xmin, ymin = np.min(vertices, axis=0)
xmax, ymax = np.max(vertices, axis=0)
img = ax.imshow(
map_matrix,
cmap=colormap,
extent=(xmin, xmax, ymin, ymax),
origin='lower',
aspect='auto',
clip_path=patch,
clip_on=True,
zorder=zorder,
alpha=alpha,
rasterized=rasterized,
vmin=-1, vmax=1
)
return img
[docs]
def equidistant(points):
"""
Resamples points along a curve to make them equidistant while preserving the overall shape.
Parameters
points : :class:`numpy.ndarray`
1D array of points (x,y) defining the reference curve.
Returns
:class:`numpy.ndarray` : 2D array
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
import numpy as np
# Create a non-uniform curve
original_points = np.array([[0, 0], [1, 2], [3, 1], [4, 4]])
# Make points equidistant
equal_points = chu.equidistant(original_points)
print(equal_points)
.. code-block:: python
:class: out-block
[[0. 0. ]
[1.27614237 1.86192881]
[3.19526215 1.58578644]
[4. 4. ]]
"""
# Cumulative distances between consecutive points
diffs = np.diff(points, axis=0)
distances = np.linalg.norm(diffs, axis=1)
cumulative_length = np.insert(np.cumsum(distances), 0, 0)
# Interpolation
total_length = cumulative_length[-1]
new_distances = np.linspace(0, total_length, len(points))
interp_x = interp1d(cumulative_length, points[:, 0], kind='linear')
interp_y = interp1d(cumulative_length, points[:, 1], kind='linear')
# Equidistant curve
new_x = interp_x(new_distances)
new_y = interp_y(new_distances)
return np.column_stack((new_x, new_y))
[docs]
def quadratic_bezier(t,P0,P1,P2):
"""
Evaluates a
`quadratic Bézier curve <https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Quadratic_curves>`_
at parameter ``t`` using three control points.
Parameters
t : :class:`float`
Parameter value between 0.0 and 1.0.
P0 : :class:`tuple` or :class:`array-like`
Starting control point (x,y).
P1 : :class:`tuple` or :class:`array-like`
Middle control point (x,y).
P2 : :class:`tuple` or :class:`array-like`
Ending control point (x,y).
Returns
:class:`numpy.ndarray` : point (x,y)
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
# Evaluate curve at midpoint
point = chu.quadratic_bezier(0.5, (0, 0), (1, 2), (3, 1))
print(point)
.. code-block:: python
:class: out-block
[1.25 1.25]
"""
if any(p is None for p in [P0, P1, P2]): raise ValueError('Points cannot be None')
P0, P1, P2 = map(np.array, [P0, P1, P2])
return (1-t)**2 * P0 + 2*(1-t)*t * P1 + t**2 * P2
[docs]
def cubic_bezier(t,P0,P1,P2,P3):
"""
Evaluates a
`cubic Bézier curve <https://en.wikipedia.org/wiki/B%C3%A9zier_curve#Higher-order_curves>`_
at parameter ``t`` using four control points.
Parameters
t : :class:`float`
Parameter value between 0.0 and 1.0.
P0 : :class:`tuple` or :class:`array-like`
Starting control point (x,y).
P1 : :class:`tuple` or :class:`array-like`
First middle control point (x,y).
P2 : :class:`tuple` or :class:`array-like`
Second middle point (x,y).
P3 : :class:`tuple` or :class:`array-like`
Ending control point (x,y).
Returns
:class:`numpy.ndarray` : point (x,y)
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
# Evaluate curve at midpoint
point = chu.quadratic_bezier(0.5, (0, 0), (1, 2), (3, 1))
print(point)
.. code-block:: python
:class: out-block
[1.25 1.25]
"""
if any(p is None for p in [P0, P1, P2, P3]): raise ValueError('Points cannot be None')
P0, P1, P2, P3 = map(np.array, [P0, P1, P2, P3])
return (1-t)**3 * P0 + 3*(1-t)**2*t * P1 + 3*(1-t)*t**2 * P2 + t**3 * P3
[docs]
def get_bezier_curve(points,n=20):
"""
Generates a sequence of points along a
`Bézier curve <https://en.wikipedia.org/wiki/B%C3%A9zier_curve>`_.
Parameters
points : :class:`list` or :class:`array-like`
List containing the control points. The control points must me :class:`tuple` or
:class:`array-like` as (x,y). For quadratic Bézier curve 3 points are needed, for cubic
Bézier curve 4 points are needed.
n : :class:`int`
Number of points to generate along the curve (default: 20).
Returns
:class:`numpy.ndarray` : 1D array of points (x,y)
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
curve = chu.get_bezier_curve([(0, 0), (2, 3), (4, 1)], n=10)
print(curve)
.. code-block:: python
:class: out-block
[[0. 0. ]
[0.44444444 0.60493827]
[0.88888889 1.08641975]
[1.33333333 1.44444444]
[1.77777778 1.67901235]
[2.22222222 1.79012346]
[2.66666667 1.77777778]
[3.11111111 1.64197531]
[3.55555556 1.38271605]
[4. 1. ]]
"""
if any(p is None for p in points): raise ValueError('Points cannot be None')
t_values = np.linspace(0, 1, n)
if len(points) == 3:
P0, P1, P2 = map(np.array, points)
return np.array([quadratic_bezier(t, P0, P1, P2) for t in t_values])
elif len(points) == 4:
P0, P1, P2, P3 = map(np.array, points)
return np.array([cubic_bezier(t, P0, P1, P2, P3) for t in t_values])
else:
raise ValueError(f'Expected 3 or 4 points for Bézier curve, got {len(points)} points')
[docs]
def validate_kwargs(keys,allowed_keys,aliases={}):
"""
Validates that given keyword arguments are within the allowed set of parameters.
Parameters
keys : :class:`list` or :class:`array-like`
A list with the name of the key arguments you want to validate.
allowed_keys : :class:`list` or :class:`array-like`
A list with the name of the valid arguments.
aliases : :class:`dict`, optional
A python dictionary with alternative aliases for the key arguments ().
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
user_given_kwargs = {'name': 'Agustina', 'favorite_artist': 'Chappell Roan'}
allowed_params = ['name', 'age', 'favorite_color']
aliases = {'favorite_color': 'favc'}
chu.validate_kwargs(user_given_kwargs.keys(),allowed_params,aliases)
.. code-block:: python-console
:class: out-block
---------------------------------------------------------------------------
Traceback (most recent call last):
line 8
KeyError: 'Invalid argument "favorite_artist". Allowed arguments are: name, age, favorite_color / favc.'
"""
for key in keys:
if key not in allowed_keys:
raise KeyError(
f'Invalid argument "{key}". Allowed arguments are: '
f'{kwargs_as_string(allowed_keys,aliases)}.'
)
[docs]
def kwargs_as_string(keys,aliases={}):
"""
Formats keyword argument names as a readable string separated by ``,``, with optional aliases.
Parameters
keys : :class:`list` or :class:`array-like`
A list with the name of the key arguments you want to format as string.
aliases : :class:`dict`, optional
A python dictionary with alternative aliases for the key arguments ().
Returns
:class:`str`
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
params = {'name': 'Fabian', 'age': 21, 'favorite_color': 'blue'}
aliases = {'favorite_color': 'favc'}
my_kwargs = chu.kwargs_as_string(params.keys(),aliases)
print(f"The arguments of my function are {my_kwargs}.")
.. code-block:: text
:class: out-block
"The arguments of my function are name, age, favorite_color / favc."
"""
if not isinstance(keys,list): keys = list(keys)
for i,key in enumerate(keys):
if key in aliases.keys(): keys[i] = f'{key} / {aliases[key]}'
return ', '.join(keys)
[docs]
def mod_color(color,light=1.0,sat=1.0,alpha=1.0,alpha_bg=(1.0,1.0,1.0)):
"""
Applies multiple color modifications (lightness, saturation, transparency) to an RGB color.
Parameters
color : :class:`tuple` or :class:`array-like`
Triplet with the three RGB values (in arithmetic notation, i.e. 0.0 to 1.0)
light : :class:`float`
Lightness factor. 1.0 means no change.
sat : :class:`float`
Saturation factor. 1.0 means no change.
alpha : :class:`float`
Transparency level (0.0 to 1.0).
alpha_bg : :class:`tuple` or :class:`array-like`
Background color. Since transparency isn't actually achieved, a background color is set.
The color is combined with the background color. Default is white.
Returns
:class:`tuple` : RGB color
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
import matplotlib.colors as mcolors
# matplotlib.colors.to_rgb also supports HEX codes
my_color = mcolors.to_rgb("violet")
# Make it 50% darker, saturate to 150% and set the transparency to 30%
new_color = chu.mod_color(my_color,light=0.5,sat=1.5,alpha=0.3)
print("my_color:",my_color)
print("new_color:",new_color)
.. code-block:: text
:class: out-block
my_color: (0.9333333333333333, 0.5098039215686274, 0.9333333333333333)
new_color: (0.8558823529411764, 0.7605882352941176, 0.8558823529411764)
"""
new_color = color
# Light
if light > 1:
new_color = brighter_color(new_color,factor=light-1)
elif light < 1:
new_color = darker_color(new_color,factor=1-light)
# Saturation
new_color = saturate_color(new_color,factor=sat)
# Alpgha
new_color = alpha_color(new_color,alpha=alpha,bg=alpha_bg)
return new_color
[docs]
def brighter_color(color,factor=0.0):
"""
Increases the brightness of an RGB color by a specified factor.
Parameters
color : :class:`tuple` or :class:`array-like`
Triplet with the three RGB values (in arithmetic notation, i.e. 0.0 to 1.0)
factor : :class:`float`
Brightness factor. 0.0 means no change.
Returns
:class:`tuple` : RGB color
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
import matplotlib.colors as mcolors
# matplotlib.colors.to_rgb also supports HEX codes
my_color = mcolors.to_rgb("violet")
# Make it 50% brighter
new_color = chu.brighter_color(my_color,factor=0.5)
print("my_color:",my_color)
print("new_color:",new_color)
.. code-block:: text
:class: out-block
my_color: (0.9333333333333333, 0.5098039215686274, 0.9333333333333333)
new_color: (0.9666666666666667, 0.7549019607843137, 0.9666666666666667)
"""
r,g,b = color
factor = max(0, factor)
r = min(1, r + (1-r)*factor)
g = min(1, g + (1-g)*factor)
b = min(1, b + (1-b)*factor)
return (r,g,b)
[docs]
def darker_color(color,factor=0.0):
"""
Decreases the brightness of an RGB color by a specified factor.
Parameters
color : :class:`tuple` or :class:`array-like`
Triplet with the three RGB values (in arithmetic notation, i.e. 0.0 to 1.0)
factor : :class:`float`
Darkness factor. 0.0 means no change.
Returns
:class:`tuple` : RGB color
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
import matplotlib.colors as mcolors
# matplotlib.colors.to_rgb also supports HEX codes
my_color = mcolors.to_rgb("violet")
# Make it 50% darker
new_color = chu.darker_color(my_color,factor=0.5)
print("my_color:",my_color)
print("new_color:",new_color)
.. code-block:: text
:class: out-block
my_color: (0.9333333333333333, 0.5098039215686274, 0.9333333333333333)
new_color: (0.4666666666666667, 0.2549019607843137, 0.4666666666666667)
"""
r,g,b = color
factor = 1 - (max(0,factor))
r = max(0, r*factor)
g = max(0, g*factor)
b = max(0, b*factor)
return (r,g,b)
[docs]
def saturate_color(color,factor=1.0):
"""
Adjusts the saturation level of an RGB color by a specified factor.
Parameters
color : :class:`tuple` or :class:`array-like`
Triplet with the three RGB values (in arithmetic notation, i.e. 0.0 to 1.0)
factor : :class:`float`
Saturation factor. 1.0 means no change.
Returns
:class:`tuple` : RGB color
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
import matplotlib.colors as mcolors
# matplotlib.colors.to_rgb also supports HEX codes
my_color = mcolors.to_rgb("violet")
# Saturate to 150%
new_color = chu.saturate_color(my_color,factor=1.5)
print("my_color:",my_color)
print("new_color:",new_color)
.. code-block:: text
:class: out-block
my_color: (0.9333333333333333, 0.5098039215686274, 0.9333333333333333)
new_color: (1.0, 0.44313725490196076, 0.9999999999999999)
"""
r,g,b = color
h,l,s = colorsys.rgb_to_hls(r,g,b)
s_new = max(0, min(1,s*factor))
r,g,b = colorsys.hls_to_rgb(h,l,s_new)
return (r,g,b)
[docs]
def alpha_color(color, alpha=1.0, bg=(1.0,1.0,1.0)):
"""
Simulates transparency by blending an RGB color with a background color.
Parameters
color : :class:`tuple` or :class:`array-like`
Triplet with the three RGB values (in arithmetic notation, i.e. 0.0 to 1.0)
alpha : :class:`float`
Transparency level (0.0 to 1.0).
bg : :class:`tuple` or :class:`array-like`
Background color. Since transparency isn't actually achieved, a background color is set.
The color is combined with the background color. Default is white.
Returns
:class:`tuple` : RGB color
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
import matplotlib.colors as mcolors
# matplotlib.colors.to_rgb also supports HEX codes
my_color = mcolors.to_rgb("violet")
# Set the transparency to 30%
new_color = chu.alpha_color(my_color,alpha=0.3)
print("my_color:",my_color)
print("new_color:",new_color)
.. code-block:: text
:class: out-block
my_color: (0.9333333333333333, 0.5098039215686274, 0.9333333333333333)
new_color: (0.98, 0.8529411764705882, 0.98)
"""
r,g,b = color
bg_r,bg_g,bg_b = bg
factor = max(0, min(1, alpha))
r_result = (r*factor) + (bg_r*(1-factor))
g_result = (g*factor) + (bg_g*(1-factor))
b_result = (b*factor) + (bg_b*(1-factor))
return (r_result,g_result,b_result)
# f-string pre-defined colors
_fstr_colors = {'white':255,'black':232,'light_gray':245,'dark_gray':237,'gold':220,
'red':196,'blue':21,'green':118,'magenta':165,'mint':87,'orange':202}
[docs]
def strcol(string,c="white"):
"""
Applies ANSI color codes to a string (only work in terminal/output cells).
Parameters
string : :class:`str`
The string you want to color.
c : :class:`str` or :class:`int`
Color to color the string with. This can be a number from the ANSI 8-bit color codes
(between 0 and 255) or a string of one of the predefined colors: ``"white"``,
``"black"``, ``"light_gray"``, ``"dark_gray"``, ``"gold"``,
``"red"``, ``"blue"``, ``"green"``, ``"magenta"``, ``"mint"``,
``"orange"``.
Returns
:class:`str`
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: in-block
import cachai.utilities as chu
print(chu.strcol("Magenta text",c="magenta"))
.. raw:: html
<div class="out-block highlight-text notranslate">
<div class="highlight">
<pre><span style="color:#d700ff;background:#F5FAF7;font-size:12px">Magenta text</span></pre>
</div>
</div>
"""
if isinstance(c,int):
if (c >= 0) and (c <= 255): return f'\033[38;5;{c}m{string}\033[0m'
else: return string
else:
if c not in _fstr_colors: return string
return f'\033[38;5;{_fstr_colors[c]}m{string}\033[0m'
[docs]
def strcol_palette():
"""
Displays a visual palette of all available ANSI 256-color codes for terminal text coloring.
Examples
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python
:class: mock-block
import cachai.utilities as chu
chu.strcol_palette()
"""
for i in range(16):
for j in range(16):
print(strcol('■',c=i+j*16) + f' {str(i+j*16):<3.3} ',end='')
print()