Compare commits
5 Commits
048d1c0d96
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
6c9352f0f0
|
|||
|
d72251852e
|
|||
|
dfca46b5c3
|
|||
|
fe428f1e05
|
|||
|
4201899545
|
@@ -1,10 +1,11 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "newplot"
|
name = "vectise"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "Add your description here"
|
description = "create vectorised plots"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"numpy>=2.1.3",
|
"numpy>=2.1.3",
|
||||||
|
"pyment>=0.3.3",
|
||||||
"svg-py>=1.5.0",
|
"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
|
from abc import ABC, abstractmethod
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
from numpy.typing import ArrayLike
|
||||||
|
|
||||||
# the following functions are taken from Ben Southgate:
|
# the following functions are taken from Ben Southgate:
|
||||||
# https://bsouthga.dev/posts/colour-gradients-with-python
|
# https://bsouthga.dev/posts/colour-gradients-with-python
|
||||||
|
|
||||||
|
|
||||||
def hex_to_RGB(hex):
|
def hex_to_RGB(hex_string: str):
|
||||||
""" "#FFFFFF" -> [255,255,255]"""
|
""" "#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
|
# 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):
|
def RGB_to_hex(RGB: list[int]):
|
||||||
"""[255,255,255] -> "#FFFFFF" """
|
"""[255,255,255] -> "#FFFFFF"
|
||||||
|
|
||||||
|
:param RGB:
|
||||||
|
:type RGB: list[int] :
|
||||||
|
:param RGB: list[int]:
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
# Components need to be integers for hex to make sense
|
# Components need to be integers for hex to make sense
|
||||||
RGB = [int(x) for x in RGB]
|
RGB = [int(x) for x in RGB]
|
||||||
return "#" + "".join(
|
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
|
"""Takes in a list of RGB sub-lists and returns dictionary of
|
||||||
colours in RGB and hex form for use in a graphing function
|
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 {
|
return {
|
||||||
"hex": [RGB_to_hex(RGB) for RGB in gradient],
|
"hex": [RGB_to_hex(RGB) for RGB in gradient],
|
||||||
"r": [RGB[0] 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
|
"""returns a gradient list of (n) colours between
|
||||||
two hex colours. start_hex and finish_hex
|
two hex colours. start_hex and finish_hex
|
||||||
should be the full six-digit colour string,
|
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
|
# Starting and ending colours in RGB form
|
||||||
s = hex_to_RGB(start_hex)
|
s = hex_to_RGB(start_hex)
|
||||||
f = hex_to_RGB(finish_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)
|
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
|
"""returns a list of colours forming linear gradients between
|
||||||
all sequential pairs of colours. "n" specifies the total
|
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
|
# The number of colours per individual linear gradient
|
||||||
n_out = int(float(n) / (len(colours) - 1))
|
n_out = int(float(n) / (len(colours) - 1))
|
||||||
# returns dictionary defined by colour_dict()
|
# returns dictionary defined by colour_dict()
|
||||||
@@ -73,16 +119,21 @@ def polylinear_gradient(colours, n):
|
|||||||
|
|
||||||
|
|
||||||
class ColourMap(ABC):
|
class ColourMap(ABC):
|
||||||
|
""" """
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __call__(self, v: float): ...
|
def __call__(self, v: float): ...
|
||||||
|
|
||||||
|
|
||||||
class LinearGradientColourMap(ColourMap):
|
class LinearGradientColourMap(ColourMap):
|
||||||
|
""" """
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
colours: list[str] | None = ["#ff0000", "#ffffff", "#0000ff"],
|
colours: list[str] | None = ["#ff0000", "#ffffff", "#0000ff"],
|
||||||
min_value: float | None = 0,
|
min_value: float = 0,
|
||||||
max_value: float | None = 1,
|
max_value: float = 1,
|
||||||
|
matrix: ArrayLike | None = None,
|
||||||
bins: int = 100,
|
bins: int = 100,
|
||||||
):
|
):
|
||||||
self.colours = polylinear_gradient(colours, bins)
|
self.colours = polylinear_gradient(colours, bins)
|
||||||
@@ -96,8 +147,29 @@ class LinearGradientColourMap(ColourMap):
|
|||||||
|
|
||||||
|
|
||||||
class RandomColourMap(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)
|
self.rgen = np.random.default_rng(random_state)
|
||||||
|
|
||||||
def __call__(self, v: float):
|
def __call__(self, v: float):
|
||||||
return RGB_to_hex([x * 255 for x in self.rgen.random(3)])
|
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
|
import numpy as np
|
||||||
|
from numpy.typing import ArrayLike
|
||||||
import svg
|
import svg
|
||||||
from colours import ColourMap
|
from .colours import ColourMap
|
||||||
|
|
||||||
|
|
||||||
class MatrixVisualisation:
|
class MatrixVisualisation:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
matrix: np.typing.NDArray,
|
|
||||||
cmap: ColourMap,
|
cmap: ColourMap,
|
||||||
|
matrix: ArrayLike,
|
||||||
text: bool = False,
|
text: bool = False,
|
||||||
labels: int | list[str] | bool = False,
|
labels: int | list[str] | bool = False,
|
||||||
):
|
):
|
||||||
@@ -43,12 +44,12 @@ class MatrixVisualisation:
|
|||||||
if text:
|
if text:
|
||||||
self.elements.append(
|
self.elements.append(
|
||||||
svg.Text(
|
svg.Text(
|
||||||
x=x + width / 5,
|
x=x + width / 3,
|
||||||
y=y + 3 * height / 4,
|
y=y + 3 * height / 4,
|
||||||
textLength=width / 2,
|
# textLength=width / 2,
|
||||||
lengthAdjust="spacingAndGlyphs",
|
# lengthAdjust="spacingAndGlyphs",
|
||||||
class_=["mono"],
|
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,
|
width: int = 20,
|
||||||
resolution: int = 256,
|
resolution: int = 256,
|
||||||
border: int | bool = 1,
|
border: int | bool = 1,
|
||||||
labels: int | list[str] | bool = False,
|
labels: int | list[str | int] | bool = False,
|
||||||
):
|
):
|
||||||
if height is None:
|
if height is None:
|
||||||
height = int(self.total_height * 2 / 3)
|
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
|
version = 1
|
||||||
requires-python = ">=3.12"
|
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]]
|
[[package]]
|
||||||
name = "numpy"
|
name = "numpy"
|
||||||
version = "2.1.3"
|
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 },
|
{ 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]]
|
[[package]]
|
||||||
name = "svg-py"
|
name = "svg-py"
|
||||||
version = "1.5.0"
|
version = "1.5.0"
|
||||||
@@ -62,3 +56,20 @@ sdist = { url = "https://files.pythonhosted.org/packages/b9/6d/4680d284924d66082
|
|||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/51/2b/a541bed029de9ac6a558d3eeeeb0fff5046687651e61d38df3dc43f48e2a/svg_py-1.5.0-py3-none-any.whl", hash = "sha256:5c467efd5a43e8df4dd6f6d078063f195a04966721d36c43d62f5d7529f908d1", size = 13443 },
|
{ 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