Skip to content
Merged
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
771 changes: 390 additions & 381 deletions poetry.lock

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ style = "pep440"
[tool.poetry.group.dev.dependencies]
wheel = "^0.46.3"
types-toml = "^0.10.8.20240310"
pytest = "^9.0.2"
pytest = "^9.0.3"
mypy = "^1.19.1"
flake8 = "^7.3.0"
toml = "^0.10.2"
Expand All @@ -41,7 +41,7 @@ matplotlib = [
{ version = ">=3.10.0", python = ">=3.10,<3.14" },
{ version = ">=3.10.5", python = ">=3.10,<3.15" },
]
pillow = ">=12.1.1"
pillow = ">=12.2.0"
pyyaml = ">=6.0.0"

[tool.poetry.scripts]
Expand Down
54 changes: 54 additions & 0 deletions src/signaloid/circuitpython/extended_ulab_numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,5 +261,59 @@ def insert(self, arr1, index, arr2):

return res

def histogram(self, a, bins, weights=None):
"""
Compute the histogram of a dataset.

Mirrors the subset of ``numpy.histogram`` semantics required by
this project: each value is placed in the bin where
``bins[i] <= v < bins[i+1]``, with the last bin closed on the
right (``bins[-2] <= v <= bins[-1]``). Values outside
``[bins[0], bins[-1]]`` are excluded. ``bins`` must be a
monotonically increasing sequence of bin edges.

:param a: The input values.
:param bins: A monotonically increasing sequence of bin edges
(length ``n_bins + 1``).
:param weights: Optional per-element weights. If ``None``, each
value contributes 1.0 to its bin.

:return: A tuple ``(hist, bin_edges)`` where ``hist`` has length
``n_bins`` and ``bin_edges`` is the input ``bins`` as an array.
"""

edges = self.array(bins)
n_bins = len(edges) - 1
if n_bins < 1:
raise ValueError("histogram requires at least 2 bin edges")

n_values = len(a)
if weights is None:
weights = [1.0] * n_values

hist = [0.0] * n_bins
first_edge = edges[0]
last_edge = edges[-1]

for i in range(n_values):
v = a[i]
if v < first_edge or v > last_edge:
continue
# bisect_right(edges, v) - 1 gives the numpy-style bin index.
lo, hi = 0, n_bins + 1
while lo < hi:
mid = (lo + hi) // 2
if edges[mid] <= v:
lo = mid + 1
else:
hi = mid
idx = lo - 1
if idx == n_bins:
# v == edges[-1]: the last bin is closed on the right.
idx = n_bins - 1
hist[idx] += weights[i]

return self.array(hist), edges


np = NumpyWrapper()
2 changes: 1 addition & 1 deletion src/signaloid/distributional/dirac_delta.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from __future__ import annotations

import sys
import math

Expand Down
2 changes: 1 addition & 1 deletion src/signaloid/distributional/distributional.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from __future__ import annotations

import math
import sys
import re
Expand Down
2 changes: 0 additions & 2 deletions src/signaloid/distributional/distributional_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
# DEALINGS IN THE SOFTWARE.


from __future__ import annotations

import csv
import os
import unittest
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from __future__ import annotations

import sys
import math

Expand Down Expand Up @@ -61,7 +61,7 @@ def from_samples(
cls,
samples: np.ndarray | list[float],
plotting_resolution: int | None = None,
) -> PlotData:
) -> "PlotData":
"""
Construct a PlotData from an array of float samples.

Expand Down Expand Up @@ -600,33 +600,48 @@ def _construct_plot_data(self) -> None:
"plot_histogram_dirac_deltas: plotting_resolution must be a power of 2!"
)

try:
# Create the binning such that the average of two bins surrounding a Dirac delta
# is the Dirac delta itself.
boundary_positions, bin_widths, bin_heights = PlotData.create_binning(
finite_dirac_deltas, 0, False
)

# Find the TTR of the created binning. This is always a valid TTR.
ttr = PlotData._bin_pdf_to_ttr(
boundary_positions, bin_widths, bin_heights, self.plotting_ttr_order
)

# Create the binning from the obtained (valid) TTR using the TTR binning method.
boundary_positions, bin_widths, bin_heights = PlotData.create_binning(
ttr, self.plotting_ttr_order, True
)
if self.dist.check_is_full_valid_TTR():
try:
# Create the binning such that the average of two bins surrounding a Dirac delta
# is the Dirac delta itself.
boundary_positions, bin_widths, bin_heights = PlotData.create_binning(
finite_dirac_deltas, 0, False
)

self.positions = boundary_positions
self.masses = bin_heights
except (ValueError, TypeError):
positions = np.array([dd.position for dd in finite_dirac_deltas])
masses = np.array([dd.mass for dd in finite_dirac_deltas])
# Find the TTR of the created binning. This is always a valid TTR.
ttr = PlotData._bin_pdf_to_ttr(
boundary_positions,
bin_widths,
bin_heights,
self.plotting_ttr_order,
)

midpoints = (positions[:-1] + positions[1:]) / 2
left = 2 * positions[0] - midpoints[0]
right = 2 * positions[-1] - midpoints[-1]
self.positions = np.concatenate(([left], midpoints, [right]))
# Create the binning from the obtained (valid) TTR using the TTR binning method.
boundary_positions, bin_widths, bin_heights = PlotData.create_binning(
ttr, self.plotting_ttr_order, True
)

widths = np.diff(self.positions)
self.masses = masses / widths
self.positions = boundary_positions
self.masses = bin_heights
return
except (ValueError, TypeError):
pass

# Non-TTR input (or TTR pipeline failed): use a uniform-width histogram
# at `plotting_resolution`. The TTR binning method assumes the input
# forms a valid TTR, so applying it to arbitrary Dirac deltas (e.g. raw
# samples) produces meaningless boundaries.
positions = np.array([dd.position for dd in finite_dirac_deltas])
masses = np.array([dd.mass for dd in finite_dirac_deltas])
pos_min = float(positions[0])
pos_max = float(positions[-1])
edges = np.linspace(pos_min, pos_max, self.plotting_resolution + 1)
hist, _ = np.histogram(positions, bins=edges, weights=masses)
bin_widths = edges[1:] - edges[:-1]
self.positions = edges
self.masses = np.divide(
hist,
bin_widths,
out=np.zeros_like(hist, dtype=np.float64),
where=bin_widths != 0,
)
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,13 @@
# DEALINGS IN THE SOFTWARE.


import os
import random
import unittest

import numpy as np
from signaloid.distributional.dirac_delta import DiracDelta
from signaloid.distributional.distributional import DistributionalValue
from signaloid.distributional_information_plotting.plot_histogram_dirac_deltas import (
PlotData,
)
Expand Down Expand Up @@ -283,6 +285,65 @@ def test_plot_from_samples_with_special_values_succeeds(self) -> None:
self.assertTrue(result)


class TestNonTTRHistogramFallback(unittest.TestCase):
"""Covers the uniform-width histogram fallback used when
`is_full_valid_TTR` is False or the TTR pipeline raises."""

@staticmethod
def _fixture_path(name: str) -> str:
here = os.path.realpath(os.path.dirname(__file__))
return os.path.join(here, name)

def test_invalid_ttr_ux_string_falls_back_to_uniform_histogram(self) -> None:
"""The fixture is a Ux string that is not a valid TTR; the fallback
should produce a uniform-width histogram with shape matching
plotting_resolution."""
with open(self._fixture_path("invalid_ttr_ux_string.dat")) as f:
ux_data = f.read().strip()
dist = DistributionalValue.parse(ux_data)
self.assertIsNotNone(dist)
assert dist is not None
self.assertFalse(dist.is_full_valid_TTR)

pd = PlotData(dist)

self.assertIsNotNone(pd.plotting_resolution)
assert pd.plotting_resolution is not None
self.assertEqual(len(pd.positions), pd.plotting_resolution + 1)
self.assertEqual(len(pd.masses), pd.plotting_resolution)

widths = pd.positions[1:] - pd.positions[:-1]
self.assertTrue(np.allclose(widths, widths[0]))

# Density should integrate to ~1 over the finite-mass region.
total_area = float(np.sum(widths * pd.masses))
self.assertAlmostEqual(total_area, 1.0, places=6)

def test_clustered_repeated_positions_fallback_succeeds(self) -> None:
"""Heavy duplication / tight clusters must not crash the fallback;
shape and density invariants still hold."""
samples = np.concatenate(
[
np.full(200, 1.0),
np.full(200, 2.0),
np.full(200, 3.0),
np.full(50, 2.0 + 1e-12),
np.full(50, 2.0 - 1e-12),
]
)
pd = PlotData.from_samples(samples)

self.assertIsNotNone(pd.plotting_resolution)
assert pd.plotting_resolution is not None
self.assertEqual(len(pd.positions), pd.plotting_resolution + 1)
self.assertEqual(len(pd.masses), pd.plotting_resolution)

widths = pd.positions[1:] - pd.positions[:-1]
self.assertTrue(np.allclose(widths, widths[0]))
total_area = float(np.sum(widths * pd.masses))
self.assertAlmostEqual(total_area, 1.0, places=6)


def dirac_deltas_to_ttr(
dirac_deltas: list[DiracDelta], order: int, count: int = 0
) -> list[DiracDelta]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.

from __future__ import annotations

import math
from typing import Any
import matplotlib
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
# DEALINGS IN THE SOFTWARE.


from __future__ import annotations

import csv
import os
import shutil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
# DEALINGS IN THE SOFTWARE.


from __future__ import annotations
from signaloid.distributional.distributional import DistributionalValue
from signaloid.distributional_information_plotting.plot_histogram_dirac_deltas import (
PlotData,
Expand Down
1 change: 0 additions & 1 deletion src/signaloid/uxdata_toolkit.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@
- pip install git+https://github.com/signaloid/signaloid-python
"""

from __future__ import annotations
import argparse
import sys
import traceback
Expand Down
Loading