Compare commits
5 Commits
048d1c0d96
...
6c9352f0f0
| Author | SHA1 | Date | |
|---|---|---|---|
|
6c9352f0f0
|
|||
|
d72251852e
|
|||
|
dfca46b5c3
|
|||
|
fe428f1e05
|
|||
|
4201899545
|
@@ -1,10 +1,11 @@
|
||||
[project]
|
||||
name = "newplot"
|
||||
name = "vectise"
|
||||
version = "0.1.0"
|
||||
description = "Add your description here"
|
||||
description = "create vectorised plots"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"numpy>=2.1.3",
|
||||
"pyment>=0.3.3",
|
||||
"svg-py>=1.5.0",
|
||||
]
|
||||
|
||||
0
src/vectise/__init__.py
Normal file
0
src/vectise/__init__.py
Normal file
@@ -1,17 +1,33 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
# the following functions are taken from Ben Southgate:
|
||||
# https://bsouthga.dev/posts/colour-gradients-with-python
|
||||
|
||||
|
||||
def hex_to_RGB(hex):
|
||||
""" "#FFFFFF" -> [255,255,255]"""
|
||||
def hex_to_RGB(hex_string: str):
|
||||
""" "#FFFFFF" -> [255,255,255]
|
||||
|
||||
:param hex_string:
|
||||
:type hex_string: str :
|
||||
:param hex_string: str:
|
||||
|
||||
|
||||
"""
|
||||
# Pass 16 to the integer function for change of base
|
||||
return [int(hex[i : i + 2], 16) for i in range(1, 6, 2)]
|
||||
return [int(hex_string[i : i + 2], 16) for i in range(1, 6, 2)]
|
||||
|
||||
|
||||
def RGB_to_hex(RGB):
|
||||
"""[255,255,255] -> "#FFFFFF" """
|
||||
def RGB_to_hex(RGB: list[int]):
|
||||
"""[255,255,255] -> "#FFFFFF"
|
||||
|
||||
:param RGB:
|
||||
:type RGB: list[int] :
|
||||
:param RGB: list[int]:
|
||||
|
||||
|
||||
"""
|
||||
# Components need to be integers for hex to make sense
|
||||
RGB = [int(x) for x in RGB]
|
||||
return "#" + "".join(
|
||||
@@ -19,10 +35,17 @@ def RGB_to_hex(RGB):
|
||||
)
|
||||
|
||||
|
||||
def colour_dict(gradient):
|
||||
def colour_dict(gradient: dict):
|
||||
"""Takes in a list of RGB sub-lists and returns dictionary of
|
||||
colours in RGB and hex form for use in a graphing function
|
||||
defined later on."""
|
||||
defined later on.
|
||||
|
||||
:param gradient:
|
||||
:type gradient: dict :
|
||||
:param gradient: dict:
|
||||
|
||||
|
||||
"""
|
||||
return {
|
||||
"hex": [RGB_to_hex(RGB) for RGB in gradient],
|
||||
"r": [RGB[0] for RGB in gradient],
|
||||
@@ -31,11 +54,24 @@ def colour_dict(gradient):
|
||||
}
|
||||
|
||||
|
||||
def linear_gradient(start_hex, finish_hex="#FFFFFF", n=10):
|
||||
def linear_gradient(start_hex: str, finish_hex: str = "#FFFFFF", n: int = 10):
|
||||
"""returns a gradient list of (n) colours between
|
||||
two hex colours. start_hex and finish_hex
|
||||
should be the full six-digit colour string,
|
||||
inlcuding the number sign ("#FFFFFF")"""
|
||||
inlcuding the number sign ("#FFFFFF")
|
||||
|
||||
:param start_hex:
|
||||
:type start_hex: str :
|
||||
:param finish_hex: (Default value = "#FFFFFF")
|
||||
:type finish_hex: str :
|
||||
:param n: (Default value = 10)
|
||||
:type n: int :
|
||||
:param start_hex: str:
|
||||
:param finish_hex: str: (Default value = "#FFFFFF")
|
||||
:param n: int: (Default value = 10)
|
||||
|
||||
|
||||
"""
|
||||
# Starting and ending colours in RGB form
|
||||
s = hex_to_RGB(start_hex)
|
||||
f = hex_to_RGB(finish_hex)
|
||||
@@ -53,10 +89,20 @@ def linear_gradient(start_hex, finish_hex="#FFFFFF", n=10):
|
||||
return colour_dict(RGB_list)
|
||||
|
||||
|
||||
def polylinear_gradient(colours, n):
|
||||
def polylinear_gradient(colours: list[str], n: int):
|
||||
"""returns a list of colours forming linear gradients between
|
||||
all sequential pairs of colours. "n" specifies the total
|
||||
number of desired output colours"""
|
||||
number of desired output colours
|
||||
|
||||
:param colours:
|
||||
:type colours: list[str] :
|
||||
:param n:
|
||||
:type n: int :
|
||||
:param colours: list[str]:
|
||||
:param n: int:
|
||||
|
||||
|
||||
"""
|
||||
# The number of colours per individual linear gradient
|
||||
n_out = int(float(n) / (len(colours) - 1))
|
||||
# returns dictionary defined by colour_dict()
|
||||
@@ -73,16 +119,21 @@ def polylinear_gradient(colours, n):
|
||||
|
||||
|
||||
class ColourMap(ABC):
|
||||
""" """
|
||||
|
||||
@abstractmethod
|
||||
def __call__(self, v: float): ...
|
||||
|
||||
|
||||
class LinearGradientColourMap(ColourMap):
|
||||
""" """
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
colours: list[str] | None = ["#ff0000", "#ffffff", "#0000ff"],
|
||||
min_value: float | None = 0,
|
||||
max_value: float | None = 1,
|
||||
min_value: float = 0,
|
||||
max_value: float = 1,
|
||||
matrix: ArrayLike | None = None,
|
||||
bins: int = 100,
|
||||
):
|
||||
self.colours = polylinear_gradient(colours, bins)
|
||||
@@ -96,8 +147,29 @@ class LinearGradientColourMap(ColourMap):
|
||||
|
||||
|
||||
class RandomColourMap(ColourMap):
|
||||
def __init__(self, random_state: int | list[int] | None = [2, 3, 4, 5, 6]):
|
||||
""" """
|
||||
|
||||
def __init__(self, random_state: int | list[int] | None = None):
|
||||
self.rgen = np.random.default_rng(random_state)
|
||||
|
||||
def __call__(self, v: float):
|
||||
return RGB_to_hex([x * 255 for x in self.rgen.random(3)])
|
||||
|
||||
|
||||
class BinaryColourMap(ColourMap):
|
||||
def __init__(self, colours: list[str]):
|
||||
self.colours = colours
|
||||
|
||||
def __call__(self, v: float):
|
||||
return self.colours[np.isclose(v, 0).astype(int)]
|
||||
|
||||
|
||||
class RandomChoiceColourMap(ColourMap):
|
||||
""" """
|
||||
|
||||
def __init__(self, colours: list[str], random_state: int | list[int] | None = None):
|
||||
self.colours = colours
|
||||
self.rgen = np.random.default_rng(random_state)
|
||||
|
||||
def __call__(self, v: float):
|
||||
return self.colours[int(self.rgen.random() * len(self.colours) - 1)]
|
||||
@@ -1,13 +1,14 @@
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike
|
||||
import svg
|
||||
from colours import ColourMap
|
||||
from .colours import ColourMap
|
||||
|
||||
|
||||
class MatrixVisualisation:
|
||||
def __init__(
|
||||
self,
|
||||
matrix: np.typing.NDArray,
|
||||
cmap: ColourMap,
|
||||
matrix: ArrayLike,
|
||||
text: bool = False,
|
||||
labels: int | list[str] | bool = False,
|
||||
):
|
||||
@@ -43,12 +44,12 @@ class MatrixVisualisation:
|
||||
if text:
|
||||
self.elements.append(
|
||||
svg.Text(
|
||||
x=x + width / 5,
|
||||
x=x + width / 3,
|
||||
y=y + 3 * height / 4,
|
||||
textLength=width / 2,
|
||||
lengthAdjust="spacingAndGlyphs",
|
||||
# textLength=width / 2,
|
||||
# lengthAdjust="spacingAndGlyphs",
|
||||
class_=["mono"],
|
||||
text=f"{self.matrix[i, j]:.02f}",
|
||||
text=f"{int(self.matrix[i, j])}",
|
||||
)
|
||||
)
|
||||
|
||||
@@ -60,7 +61,7 @@ class MatrixVisualisation:
|
||||
width: int = 20,
|
||||
resolution: int = 256,
|
||||
border: int | bool = 1,
|
||||
labels: int | list[str] | bool = False,
|
||||
labels: int | list[str | int] | bool = False,
|
||||
):
|
||||
if height is None:
|
||||
height = int(self.total_height * 2 / 3)
|
||||
150
src/vectise/neural_net.py
Normal file
150
src/vectise/neural_net.py
Normal file
@@ -0,0 +1,150 @@
|
||||
import svg
|
||||
import logging
|
||||
|
||||
import itertools
|
||||
import numpy as np
|
||||
from numpy.typing import ArrayLike
|
||||
|
||||
|
||||
class NeuralNet:
|
||||
def __init__(
|
||||
self,
|
||||
matrix: ArrayLike | list[ArrayLike] | None = None,
|
||||
shape: tuple[int] | None = None,
|
||||
node_colour: str = "blue",
|
||||
node_border: str = "black",
|
||||
edge_colours: list[str] = ["green"],
|
||||
):
|
||||
"""Initialise the visualisation object.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
matrix : ArrayLike | None
|
||||
matrix or list of matrices
|
||||
shape : tuple[int] | None
|
||||
shape
|
||||
"""
|
||||
if matrix is None and shape is None:
|
||||
logging.error("supply a matrix or at least its shape!")
|
||||
elif matrix is None:
|
||||
self.matrix = [
|
||||
np.ones((shape[i], shape[i + 1])) for i in range(len(shape) - 1)
|
||||
]
|
||||
self.shape = shape
|
||||
elif shape is None:
|
||||
if not isinstance(matrix, list):
|
||||
self.matrix = [matrix]
|
||||
else:
|
||||
self.matrix = matrix
|
||||
shapes = []
|
||||
for m in self.matrix:
|
||||
shapes.extend(m.shape)
|
||||
self.shape = tuple(shapes[:-1:2] + [shapes[-1]])
|
||||
else:
|
||||
logging.error(
|
||||
"not implemented. best to drop the shapes. they will"
|
||||
"get inferred from the matrices."
|
||||
)
|
||||
if len(edge_colours) != len(self.matrix):
|
||||
edge_colours = edge_colours * len(self.matrix)
|
||||
|
||||
self.elms: list[svg.Element] = []
|
||||
self.r = 12
|
||||
self.d_y = 32
|
||||
self.d_x = 200
|
||||
self.max_card = max(self.shape)
|
||||
self.max_height = (self.max_card - 1) * self.d_y + 2 * self.d_y
|
||||
|
||||
node_elms = []
|
||||
underlying_rectangles: list[svg.Element] = []
|
||||
node_coords = []
|
||||
for i_layer, card in enumerate(self.shape):
|
||||
node_coord = []
|
||||
offset = abs(self.max_card - card) / 2 * self.d_y
|
||||
underlying_rectangles.append(
|
||||
svg.Rect(
|
||||
x=self.d_y - 3 / 2 * self.r + i_layer * self.d_x,
|
||||
y=offset + self.d_y - 3 / 2 * self.r,
|
||||
width=3 * self.r,
|
||||
height=(card - 1) * self.d_y + 3 * self.r,
|
||||
rx=self.r,
|
||||
fill="lightgrey",
|
||||
)
|
||||
)
|
||||
for i in range(card):
|
||||
node_elms.append(
|
||||
svg.Circle(
|
||||
cx=self.d_y + i_layer * self.d_x,
|
||||
cy=offset + self.d_y + self.d_y * i,
|
||||
r=self.r,
|
||||
stroke=node_border,
|
||||
fill=node_colour,
|
||||
stroke_width=1,
|
||||
)
|
||||
)
|
||||
node_coord.append(
|
||||
(self.d_y + i_layer * self.d_x, offset + self.d_y + self.d_y * i)
|
||||
)
|
||||
node_coords.append(node_coord)
|
||||
|
||||
arrowhead = svg.Defs(
|
||||
elements=[
|
||||
svg.Marker(
|
||||
id="arrowhead",
|
||||
elements=[
|
||||
svg.Path(
|
||||
d=[svg.M(0, 0), svg.L(10, 5), svg.L(0, 10), svg.Z()],
|
||||
fill="green",
|
||||
)
|
||||
],
|
||||
markerWidth=6,
|
||||
markerHeight=6,
|
||||
orient="auto-start-reverse",
|
||||
refX=8,
|
||||
refY=5,
|
||||
viewBox=svg.ViewBoxSpec(0, 0, 10, 10),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
edges = []
|
||||
for i_layer in range(len(node_coords) - 1):
|
||||
for (i, p1), (j, p2) in itertools.product(
|
||||
enumerate(node_coords[i_layer]), enumerate(node_coords[i_layer + 1])
|
||||
):
|
||||
if (self.matrix[i_layer][i, j] == 0).all():
|
||||
continue
|
||||
edges.append(
|
||||
svg.Path(
|
||||
d=[
|
||||
svg.M(p1[0] + self.r, p1[1]),
|
||||
svg.L(p2[0] - self.r, p2[1]),
|
||||
],
|
||||
stroke=edge_colours[i_layer],
|
||||
# marker_end="url(#arrowhead)",
|
||||
)
|
||||
)
|
||||
|
||||
self.elms.append(arrowhead)
|
||||
self.elms.append(svg.Style(text=".mono {font: monospace; text-align: center;}"))
|
||||
self.elms.append(svg.Style(text=".small {font-size: 25%;}"))
|
||||
self.elms.append(svg.Style(text=".normal {font-size: 12px;}"))
|
||||
|
||||
self.elms.extend(underlying_rectangles)
|
||||
self.elms.extend(edges)
|
||||
self.elms.extend(node_elms)
|
||||
|
||||
@property
|
||||
def svg(self):
|
||||
return str(
|
||||
svg.SVG(
|
||||
width=(len(self.shape) - 1) * self.d_x + 2 * self.d_y,
|
||||
height=self.max_height,
|
||||
elements=self.elms,
|
||||
)
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"""Neural Net Visualisation:
|
||||
shape: {self.shape}
|
||||
"""
|
||||
41
uv.lock
generated
41
uv.lock
generated
@@ -1,21 +1,6 @@
|
||||
version = 1
|
||||
requires-python = ">=3.12"
|
||||
|
||||
[[package]]
|
||||
name = "newplot"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "svg-py" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "numpy", specifier = ">=2.1.3" },
|
||||
{ name = "svg-py", specifier = ">=1.5.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.1.3"
|
||||
@@ -54,6 +39,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/86/09/a5ab407bd7f5f5599e6a9261f964ace03a73e7c6928de906981c31c38082/numpy-2.1.3-cp313-cp313t-win_amd64.whl", hash = "sha256:2564fbdf2b99b3f815f2107c1bbc93e2de8ee655a69c261363a1172a79a257d4", size = 12644098 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyment"
|
||||
version = "0.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/dd/9e/c58a151c7020f6fdd48eea0085a9d1c91a57da19fa4e7bff0daf930c9900/Pyment-0.3.3.tar.gz", hash = "sha256:951a4c52d6791ccec55bc739811169eed69917d3874f5fe722866623a697f39d", size = 21003 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/52/01/810e174c28a7dcf5f91c048faf69c84eafee60c9a844e4ce21671b2e99bb/Pyment-0.3.3-py2.py3-none-any.whl", hash = "sha256:a0c6ec59d06d24aeec3eaecb22115d0dc95d09e14209b2df838381fdf47a78cc", size = 21924 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "svg-py"
|
||||
version = "1.5.0"
|
||||
@@ -62,3 +56,20 @@ sdist = { url = "https://files.pythonhosted.org/packages/b9/6d/4680d284924d66082
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/2b/a541bed029de9ac6a558d3eeeeb0fff5046687651e61d38df3dc43f48e2a/svg_py-1.5.0-py3-none-any.whl", hash = "sha256:5c467efd5a43e8df4dd6f6d078063f195a04966721d36c43d62f5d7529f908d1", size = 13443 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "vectise"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "numpy" },
|
||||
{ name = "pyment" },
|
||||
{ name = "svg-py" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "numpy", specifier = ">=2.1.3" },
|
||||
{ name = "pyment", specifier = ">=0.3.3" },
|
||||
{ name = "svg-py", specifier = ">=1.5.0" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user