hgraph-analytics 0.8.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,7 @@
1
+ /build/
2
+ /build-*/
3
+ /cmake-build-*/
4
+ /dist/
5
+ /.venv/
6
+ __pycache__/
7
+ *.pyc
@@ -0,0 +1,8 @@
1
+ # Changelog
2
+
3
+ ## 0.8.0
4
+
5
+ - Move `diff`, `count`, `clip`, `ewma`, `center_of_mass_to_alpha`,
6
+ `span_to_alpha`, and `pct_change` from core into `hgraph-analytics`.
7
+ - Extend the C++-first `pct_change` graph with observation-count periods and an
8
+ explicit divide-by-zero policy.
@@ -0,0 +1,117 @@
1
+ cmake_minimum_required(VERSION 3.25)
2
+
3
+ project(hgraph_analytics VERSION 0.8.0 LANGUAGES CXX)
4
+
5
+ include(GNUInstallDirs)
6
+ include(CMakePackageConfigHelpers)
7
+ include(CTest)
8
+
9
+ option(HGRAPH_ANALYTICS_BUILD_PYTHON "Build the optional Python authoring bridge" OFF)
10
+ option(HGRAPH_ANALYTICS_WARNINGS_AS_ERRORS
11
+ "Treat hgraph-analytics compiler warnings as errors"
12
+ ${HGRAPH_WARNINGS_AS_ERRORS})
13
+
14
+ if(HGRAPH_ANALYTICS_BUILD_PYTHON)
15
+ find_package(Python 3.12 COMPONENTS
16
+ Interpreter Development.Module Development.SABIModule REQUIRED)
17
+ endif()
18
+
19
+ if(NOT TARGET hgraph::core)
20
+ find_package(hgraph CONFIG REQUIRED)
21
+ endif()
22
+
23
+ if(HGRAPH_ANALYTICS_BUILD_PYTHON)
24
+ get_target_property(_hgraph_analytics_core_links hgraph::options INTERFACE_LINK_LIBRARIES)
25
+ if("Python::Python" IN_LIST _hgraph_analytics_core_links)
26
+ message(FATAL_ERROR
27
+ "The selected hgraph SDK embeds a specific Python interpreter and "
28
+ "cannot produce an ABI3 hgraph-analytics wheel; use the SDK installed "
29
+ "by an hgraph stable-ABI wheel")
30
+ endif()
31
+ endif()
32
+
33
+ add_library(hgraph_analytics
34
+ src/operators.cpp
35
+ )
36
+ add_library(hgraph::analytics ALIAS hgraph_analytics)
37
+
38
+ get_target_property(HGRAPH_ANALYTICS_LIBRARY_TYPE hgraph_analytics TYPE)
39
+ if(HGRAPH_ANALYTICS_LIBRARY_TYPE STREQUAL "STATIC_LIBRARY")
40
+ target_compile_definitions(hgraph_analytics PUBLIC HGRAPH_ANALYTICS_STATIC_DEFINE)
41
+ endif()
42
+
43
+ target_compile_features(hgraph_analytics PUBLIC cxx_std_23)
44
+ target_link_libraries(hgraph_analytics PUBLIC hgraph::core)
45
+ target_include_directories(hgraph_analytics
46
+ PUBLIC
47
+ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
48
+ $<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
49
+ )
50
+ set_target_properties(hgraph_analytics PROPERTIES
51
+ EXPORT_NAME analytics
52
+ POSITION_INDEPENDENT_CODE ON
53
+ )
54
+
55
+ if(MSVC)
56
+ target_compile_options(hgraph_analytics PRIVATE /W4 /permissive-)
57
+ if(HGRAPH_ANALYTICS_WARNINGS_AS_ERRORS)
58
+ target_compile_options(hgraph_analytics PRIVATE /WX)
59
+ endif()
60
+ else()
61
+ target_compile_options(hgraph_analytics PRIVATE -Wall -Wextra -Wpedantic)
62
+ if(HGRAPH_ANALYTICS_WARNINGS_AS_ERRORS)
63
+ target_compile_options(hgraph_analytics PRIVATE -Werror)
64
+ endif()
65
+ endif()
66
+
67
+ if(BUILD_TESTING)
68
+ add_subdirectory(tests)
69
+ endif()
70
+
71
+ if(HGRAPH_ANALYTICS_BUILD_PYTHON)
72
+ if(NOT COMMAND hgraph_add_python_module OR NOT TARGET hgraph::nanobind)
73
+ message(FATAL_ERROR
74
+ "HGRAPH_ANALYTICS_BUILD_PYTHON requires a Python-enabled installed hgraph SDK")
75
+ endif()
76
+ hgraph_add_python_module(_hgraph_analytics STABLE_ABI NOMINSIZE src/python_module.cpp)
77
+ target_link_libraries(_hgraph_analytics PRIVATE hgraph::analytics)
78
+ install(TARGETS _hgraph_analytics
79
+ COMPONENT Python
80
+ LIBRARY DESTINATION hgraph_analytics
81
+ RUNTIME DESTINATION hgraph_analytics
82
+ )
83
+ endif()
84
+
85
+ install(TARGETS hgraph_analytics
86
+ EXPORT hgraphAnalyticsTargets
87
+ COMPONENT Development
88
+ ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
89
+ LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
90
+ RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
91
+ )
92
+ install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}
93
+ COMPONENT Development FILES_MATCHING PATTERN "*.h")
94
+
95
+ write_basic_package_version_file(
96
+ "${PROJECT_BINARY_DIR}/hgraph-analyticsConfigVersion.cmake"
97
+ VERSION ${PROJECT_VERSION}
98
+ COMPATIBILITY SameMajorVersion
99
+ )
100
+ configure_package_config_file(
101
+ "${CMAKE_CURRENT_SOURCE_DIR}/cmake/hgraph-analyticsConfig.cmake.in"
102
+ "${PROJECT_BINARY_DIR}/hgraph-analyticsConfig.cmake"
103
+ INSTALL_DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hgraph-analytics
104
+ )
105
+
106
+ install(EXPORT hgraphAnalyticsTargets
107
+ FILE hgraphAnalyticsTargets.cmake
108
+ NAMESPACE hgraph::
109
+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hgraph-analytics
110
+ COMPONENT Development
111
+ )
112
+ install(FILES
113
+ "${PROJECT_BINARY_DIR}/hgraph-analyticsConfig.cmake"
114
+ "${PROJECT_BINARY_DIR}/hgraph-analyticsConfigVersion.cmake"
115
+ DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/hgraph-analytics
116
+ COMPONENT Development
117
+ )
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 Howard Henson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,45 @@
1
+ Metadata-Version: 2.2
2
+ Name: hgraph-analytics
3
+ Version: 0.8.5
4
+ Summary: C++-first numerical analytics for hgraph
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/hhenson/hgraph
7
+ Project-URL: Repository, https://github.com/hhenson/hgraph.git
8
+ Requires-Python: >=3.12
9
+ Requires-Dist: hgraph>=0.8.0
10
+ Provides-Extra: test
11
+ Requires-Dist: pytest>=8; extra == "test"
12
+ Description-Content-Type: text/markdown
13
+
14
+ # hgraph-analytics
15
+
16
+ C++-first numerical analytics for hgraph. The package owns the numerical
17
+ analytical family migrated from core—`diff`, `count`, `clip`, `ewma`, and
18
+ `pct_change`—plus the EWMA parameter conversion helpers.
19
+
20
+ ```python
21
+ import hgraph as hg
22
+ import hgraph_analytics as hga
23
+
24
+ change = hga.pct_change(
25
+ value,
26
+ period=12,
27
+ divide_by_zero=hg.DivideByZero.NAN,
28
+ )
29
+
30
+ bounded = hga.clip(change, -0.25, 0.25)
31
+ smoothed = hga.ewma(bounded, alpha=0.2)
32
+ ```
33
+
34
+ The result is fractional: `0.05` denotes five percent. `period` counts valid
35
+ source observations and must be positive. The operator does not infer
36
+ dataframe ordering, elapsed-time sampling, market sessions, or financial price
37
+ adjustment.
38
+
39
+ Native consumers link `hgraph::analytics`, call
40
+ `hgraph::analytics::register_analytics_operators()`, and wire
41
+ the markers in `hgraph::analytics`, including `diff`, `count`, `clip`, `ewma`,
42
+ and `pct_change`.
43
+
44
+ See the hgraph user-guide migration note for the complete Python and C++ name
45
+ mapping from the former core API.
@@ -0,0 +1,32 @@
1
+ # hgraph-analytics
2
+
3
+ C++-first numerical analytics for hgraph. The package owns the numerical
4
+ analytical family migrated from core—`diff`, `count`, `clip`, `ewma`, and
5
+ `pct_change`—plus the EWMA parameter conversion helpers.
6
+
7
+ ```python
8
+ import hgraph as hg
9
+ import hgraph_analytics as hga
10
+
11
+ change = hga.pct_change(
12
+ value,
13
+ period=12,
14
+ divide_by_zero=hg.DivideByZero.NAN,
15
+ )
16
+
17
+ bounded = hga.clip(change, -0.25, 0.25)
18
+ smoothed = hga.ewma(bounded, alpha=0.2)
19
+ ```
20
+
21
+ The result is fractional: `0.05` denotes five percent. `period` counts valid
22
+ source observations and must be positive. The operator does not infer
23
+ dataframe ordering, elapsed-time sampling, market sessions, or financial price
24
+ adjustment.
25
+
26
+ Native consumers link `hgraph::analytics`, call
27
+ `hgraph::analytics::register_analytics_operators()`, and wire
28
+ the markers in `hgraph::analytics`, including `diff`, `count`, `clip`, `ewma`,
29
+ and `pct_change`.
30
+
31
+ See the hgraph user-guide migration note for the complete Python and C++ name
32
+ mapping from the former core API.
@@ -0,0 +1,8 @@
1
+ @PACKAGE_INIT@
2
+
3
+ include(CMakeFindDependencyMacro)
4
+ find_dependency(hgraph CONFIG)
5
+
6
+ include("${CMAKE_CURRENT_LIST_DIR}/hgraphAnalyticsTargets.cmake")
7
+
8
+ check_required_components(hgraph-analytics)
@@ -0,0 +1,16 @@
1
+ #ifndef HGRAPH_ANALYTICS_EXPORT_H
2
+ #define HGRAPH_ANALYTICS_EXPORT_H
3
+
4
+ #if defined(HGRAPH_ANALYTICS_STATIC_DEFINE)
5
+ #define HGRAPH_ANALYTICS_EXPORT
6
+ #elif defined(_WIN32)
7
+ #if defined(hgraph_analytics_EXPORTS)
8
+ #define HGRAPH_ANALYTICS_EXPORT __declspec(dllexport)
9
+ #else
10
+ #define HGRAPH_ANALYTICS_EXPORT __declspec(dllimport)
11
+ #endif
12
+ #else
13
+ #define HGRAPH_ANALYTICS_EXPORT __attribute__((visibility("default")))
14
+ #endif
15
+
16
+ #endif // HGRAPH_ANALYTICS_EXPORT_H
@@ -0,0 +1,124 @@
1
+ #ifndef HGRAPH_ANALYTICS_OPERATORS_H
2
+ #define HGRAPH_ANALYTICS_OPERATORS_H
3
+
4
+ #include <hgraph/analytics/export.h>
5
+
6
+ #include <hgraph/lib/std/operators/arithmetic.h>
7
+ #include <hgraph/types/operator_dispatch.h>
8
+ #include <hgraph/types/primitive_types.h>
9
+ #include <hgraph/types/static_schema.h>
10
+
11
+ namespace hgraph::analytics
12
+ {
13
+ /** Subtract the preceding valid observation from the current observation.
14
+ The first valid observation warms the retained prior-value state and does
15
+ not produce output. Invalid cycles neither update that state nor trigger.
16
+ @param ts Integer or floating-point input stream.
17
+ @return Successive differences with the same scalar type as ``ts``.
18
+ @par Python example
19
+ @code{.py}
20
+ import hgraph_analytics as hga
21
+ change = hga.diff(price)
22
+ @endcode */
23
+ struct diff
24
+ : Operator<"hgraph.analytics.diff", In<"ts", TS<ScalarVar<"T">>>,
25
+ Out<TS<ScalarVar<"T">>>>
26
+ {
27
+ };
28
+
29
+ /** Count valid input ticks cumulatively, with an optional reset signal.
30
+ A same-cycle reset is applied before a source tick is admitted. A reset-only
31
+ cycle clears state without producing output.
32
+ @param ts Signal or stream whose valid ticks are counted; values are ignored.
33
+ @param reset Optional signal that restarts the count from one on the next
34
+ same-cycle or subsequent source tick.
35
+ @return Running integer tick count.
36
+ @par Python example
37
+ @code{.py}
38
+ import hgraph_analytics as hga
39
+ session_count = hga.count(updates, reset=session_start)
40
+ @endcode */
41
+ struct count
42
+ : Operator<"hgraph.analytics.count", In<"ts", SIGNAL>, Out<TS<Int>>>
43
+ {
44
+ };
45
+
46
+ /** Constrain each numeric observation to an inclusive range.
47
+ Bounds are fixed at wiring time and must share the resolved integer or
48
+ floating-point type of the input. The node has no warm-up state and emits
49
+ for every valid source tick.
50
+ @param ts Numeric input stream.
51
+ @param min Lower inclusive bound.
52
+ @param max Upper inclusive bound.
53
+ @return ``min`` below the range, ``max`` above it, otherwise ``ts``.
54
+ @throws std::invalid_argument during node start when ``min > max``.
55
+ @par Python example
56
+ @code{.py}
57
+ import hgraph_analytics as hga
58
+ bounded = hga.clip(ratio, 0.0, 1.0)
59
+ @endcode */
60
+ struct clip
61
+ : Operator<"hgraph.analytics.clip", In<"ts", TS<ScalarVar<"T">>>,
62
+ Scalar<"min", ScalarVar<"T">>, Scalar<"max", ScalarVar<"T">>,
63
+ Out<TS<ScalarVar<"T">>>>
64
+ {
65
+ };
66
+
67
+ /** Compute an exponentially weighted moving average of floating observations.
68
+ The first valid observation initializes the retained average. Each later
69
+ valid tick emits ``alpha * current + (1 - alpha) * previous``. Invalid
70
+ cycles neither update state nor trigger output.
71
+ @param ts Floating-point input stream.
72
+ @param alpha Wiring-time smoothing factor. Values are applied as supplied
73
+ for compatibility with the migrated 0.8 operator.
74
+ @return Running exponentially weighted moving average.
75
+ @par Python example
76
+ @code{.py}
77
+ import hgraph_analytics as hga
78
+ smoothed = hga.ewma(price, alpha=0.2)
79
+ @endcode */
80
+ struct ewma
81
+ : Operator<"hgraph.analytics.ewma", In<"ts", TS<Float>>,
82
+ Scalar<"alpha", Float>, Out<TS<Float>>>
83
+ {
84
+ };
85
+
86
+ /** Compute fractional change from an earlier valid observation.
87
+ For a positive observation-count ``period``, the result is
88
+ ``(current - prior) / prior``. The first ``period`` valid observations
89
+ produce no output. Invalid source cycles are not counted, and the graph
90
+ ticks only when a valid source observation arrives.
91
+ @param ts Integer or floating-point input stream.
92
+ @param period Positive valid-observation count fixed at wiring time;
93
+ defaults to one. Durations and negative look-ahead periods
94
+ are not supported.
95
+ @param divide_by_zero Wiring-time policy applied when the prior value is
96
+ zero; defaults to ``DivideByZero::Error``.
97
+ @return Floating-point fractional change. ``0.05`` denotes five percent.
98
+ @throws std::invalid_argument while wiring when ``period`` is not positive.
99
+ @throws std::domain_error during evaluation for a zero prior value when
100
+ ``divide_by_zero`` is ``Error``.
101
+ @par Python example
102
+ @code{.py}
103
+ import hgraph as hg
104
+ import hgraph_analytics as hga
105
+
106
+ change = hga.pct_change(price, period=12,
107
+ divide_by_zero=hg.DivideByZero.NAN)
108
+ @endcode */
109
+ struct pct_change
110
+ : Operator<"hgraph.analytics.pct_change",
111
+ In<"ts", TS<ScalarVar<"T">>>,
112
+ Scalar<"period", Int>,
113
+ Scalar<"divide_by_zero", stdlib::DivideByZero>,
114
+ Out<TS<Float>>>
115
+ {
116
+ };
117
+
118
+ /** Register the hgraph-analytics overloads in the current hgraph registry.
119
+ Call once per registry lifetime after core standard operators are
120
+ available and before wiring analytics graphs. */
121
+ HGRAPH_ANALYTICS_EXPORT void register_analytics_operators();
122
+ } // namespace hgraph::analytics
123
+
124
+ #endif // HGRAPH_ANALYTICS_OPERATORS_H
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = [
3
+ "scikit-build-core>=0.11",
4
+ "nanobind==2.13.0",
5
+ "pyarrow>=25,<26",
6
+ "hgraph>=0.8.0",
7
+ ]
8
+ build-backend = "scikit_build_core.build"
9
+
10
+ [project]
11
+ name = "hgraph-analytics"
12
+ version = "0.8.5"
13
+ description = "C++-first numerical analytics for hgraph"
14
+ readme = "README.md"
15
+ license = {text = "MIT"}
16
+ requires-python = ">=3.12"
17
+ dependencies = ["hgraph>=0.8.0"]
18
+
19
+ [project.urls]
20
+ Homepage = "https://github.com/hhenson/hgraph"
21
+ Repository = "https://github.com/hhenson/hgraph.git"
22
+
23
+ [project.optional-dependencies]
24
+ test = ["pytest>=8"]
25
+
26
+ [tool.scikit-build]
27
+ cmake.source-dir = "."
28
+ cmake.args = [
29
+ "-DHGRAPH_ANALYTICS_BUILD_PYTHON=ON",
30
+ "-DBUILD_TESTING=OFF",
31
+ ]
32
+ install.components = ["Python", "Development"]
33
+ wheel.packages = ["python/hgraph_analytics"]
34
+ wheel.py-api = "cp312"
35
+ sdist.include = [
36
+ "CMakeLists.txt",
37
+ "CHANGELOG.md",
38
+ "LICENSE",
39
+ "README.md",
40
+ "cmake/**",
41
+ "include/**",
42
+ "python/**",
43
+ "src/**",
44
+ "tests/**",
45
+ "test_package/**",
46
+ "tools/**",
47
+ ]
@@ -0,0 +1,88 @@
1
+ """C++-first numerical analytics for hgraph."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from hgraph import DivideByZero, NUMBER, SIGNAL, TS, operator_function
6
+
7
+ from . import _hgraph_analytics as _native
8
+
9
+
10
+ _diff = operator_function("hgraph.analytics.diff")
11
+ _count = operator_function("hgraph.analytics.count")
12
+ _clip = operator_function("hgraph.analytics.clip")
13
+ _ewma = operator_function("hgraph.analytics.ewma")
14
+ _pct_change = operator_function("hgraph.analytics.pct_change")
15
+
16
+
17
+ def diff(ts: TS[NUMBER]) -> TS[NUMBER]:
18
+ """Return the difference from the preceding valid observation."""
19
+
20
+ return _diff(ts)
21
+
22
+
23
+ def count(ts: SIGNAL, reset: SIGNAL = None) -> TS[int]:
24
+ """Count valid input ticks, restarting when ``reset`` ticks."""
25
+
26
+ return _count(ts) if reset is None else _count(ts, reset)
27
+
28
+
29
+ def clip(ts: TS[NUMBER], min_: NUMBER, max_: NUMBER) -> TS[NUMBER]:
30
+ """Constrain each input value to the inclusive ``[min_, max_]`` range."""
31
+
32
+ return _clip(ts, min_, max_)
33
+
34
+
35
+ def ewma(ts: TS[float], alpha: float) -> TS[float]:
36
+ """Return the exponentially weighted moving average of ``ts``."""
37
+
38
+ return _ewma(ts, alpha)
39
+
40
+
41
+ def center_of_mass_to_alpha(com: float) -> float:
42
+ """Convert a positive center of mass to an EWMA smoothing factor."""
43
+
44
+ if com <= 0:
45
+ raise ValueError(f"Center of mass must be positive, got {com}")
46
+ return 1.0 / (com + 1.0)
47
+
48
+
49
+ def span_to_alpha(span: float) -> float:
50
+ """Convert a positive span to an EWMA smoothing factor."""
51
+
52
+ if span <= 0:
53
+ raise ValueError(f"Span must be positive, got {span}")
54
+ return 2.0 / (span + 1.0)
55
+
56
+
57
+ def pct_change(
58
+ ts: TS[NUMBER],
59
+ period: int = 1,
60
+ divide_by_zero: DivideByZero = DivideByZero.ERROR,
61
+ ) -> TS[float]:
62
+ """Return fractional change from ``period`` valid observations earlier.
63
+
64
+ ``0.05`` denotes five percent. The first ``period`` observations do not
65
+ produce output, invalid input cycles are not counted, and output triggers
66
+ only on a valid input tick. ``period`` is a positive wiring-time observation
67
+ count and defaults to one. ``divide_by_zero`` is a wiring-time policy for a
68
+ zero prior value and defaults to ``DivideByZero.ERROR``. The retained state
69
+ is the core lag history for ``period`` observations.
70
+
71
+ A non-positive period raises ``WiringError`` while wiring. A zero prior
72
+ raises during evaluation under the default division policy. This operator
73
+ does not infer elapsed-time, dataframe-row, sampling, or financial-return
74
+ semantics.
75
+ """
76
+
77
+ return _pct_change(ts, period, divide_by_zero)
78
+
79
+
80
+ __all__ = [
81
+ "center_of_mass_to_alpha",
82
+ "clip",
83
+ "count",
84
+ "diff",
85
+ "ewma",
86
+ "pct_change",
87
+ "span_to_alpha",
88
+ ]
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561.
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+ import sys
5
+ import zipfile
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ ANALYTICS_ROOT = Path(__file__).resolve().parents[2]
11
+ AUDIT_SCRIPT = ANALYTICS_ROOT / "tools/audit_distribution.py"
12
+
13
+
14
+ @pytest.mark.parametrize("library_directory", ["lib", "lib64"])
15
+ def test_wheel_audit_accepts_platform_library_directories(
16
+ tmp_path: Path, library_directory: str
17
+ ):
18
+ wheel = tmp_path / "hgraph_analytics-0.0.0-cp312-abi3-any.whl"
19
+ files = {
20
+ "hgraph_analytics/__init__.py": "",
21
+ "hgraph_analytics/py.typed": "",
22
+ "hgraph_analytics/_hgraph_analytics.abi3.so": "",
23
+ "include/hgraph/analytics/operators.h": "",
24
+ f"{library_directory}/libhgraph_analytics.a": "",
25
+ f"{library_directory}/cmake/hgraph-analytics/hgraph-analyticsConfig.cmake": "",
26
+ f"{library_directory}/cmake/hgraph-analytics/hgraphAnalyticsTargets.cmake": "",
27
+ "hgraph_analytics-0.0.0.dist-info/METADATA": (
28
+ "Metadata-Version: 2.2\n"
29
+ "Name: hgraph-analytics\n"
30
+ "Version: 0.0.0\n"
31
+ "Requires-Dist: hgraph\n"
32
+ ),
33
+ }
34
+ with zipfile.ZipFile(wheel, "w") as archive:
35
+ for name, contents in files.items():
36
+ archive.writestr(name, contents)
37
+
38
+ subprocess.run([sys.executable, AUDIT_SCRIPT, wheel], check=True)
@@ -0,0 +1,106 @@
1
+ import math
2
+
3
+ import pytest
4
+
5
+ import hgraph as hg
6
+ import hgraph_analytics as hga
7
+
8
+
9
+ def test_migrated_analytical_helpers():
10
+ assert hga.center_of_mass_to_alpha(1.0) == 0.5
11
+ assert hga.span_to_alpha(1.0) == 1.0
12
+ with pytest.raises(ValueError, match="Center of mass must be positive"):
13
+ hga.center_of_mass_to_alpha(0.0)
14
+ with pytest.raises(ValueError, match="Span must be positive"):
15
+ hga.span_to_alpha(0.0)
16
+
17
+
18
+ def test_diff_count_clip_and_ewma():
19
+ assert hg.eval_node(hga.diff, [1, 2, 4, 7]) == [None, 1, 2, 3]
20
+ assert hg.eval_node(hga.count, [3, None, 2, 1]) == [1, None, 2, 3]
21
+ assert hg.eval_node(
22
+ hga.count,
23
+ [3, 2, 1],
24
+ reset=[None, True, None],
25
+ resolution_dict={"ts": hg.TS[int], "reset": hg.TS[bool]},
26
+ ) == [1, 1, 2]
27
+ assert hg.eval_node(
28
+ hga.count,
29
+ [3, None, 2, 1],
30
+ reset=[None, True, None],
31
+ resolution_dict={"ts": hg.TS[int], "reset": hg.TS[bool]},
32
+ ) == [1, None, 1, 2]
33
+ assert hg.eval_node(hga.clip, [-1, 1, 3], 0, 2) == [0, 1, 2]
34
+ assert hg.eval_node(hga.clip, [-1.0, 0.5, 2.0], 0.0, 1.0) == [
35
+ 0.0,
36
+ 0.5,
37
+ 1.0,
38
+ ]
39
+ assert hg.eval_node(hga.ewma, [1.0, 2.0, 3.0, 4.0], 0.5) == [
40
+ 1.0,
41
+ 1.5,
42
+ 2.25,
43
+ 3.125,
44
+ ]
45
+
46
+
47
+ def test_count_accepts_a_mapped_dictionary_signal():
48
+ @hg.graph
49
+ def app(tsd: hg.TSD[int, hg.TS[int]]) -> hg.TS[int]:
50
+ return hga.count(hg.map_(lambda value: value + 1, tsd))
51
+
52
+ assert hg.eval_node(
53
+ app,
54
+ [{1: 10}, {2: 20}, None, {1: hg.REMOVE}],
55
+ ) == [1, 2, None, 3]
56
+
57
+
58
+ def test_clip_rejects_reversed_bounds():
59
+ with pytest.raises(Exception, match="min must be <= max"):
60
+ hg.eval_node(hga.clip, [1.0], 1.0, -1.0)
61
+
62
+
63
+ def test_default_and_sparse_observations():
64
+ assert hg.eval_node(hga.pct_change, [1, 2, 3]) == [None, 1.0, 0.5]
65
+ assert hg.eval_node(hga.pct_change, [1, None, 2]) == [None, None, 1.0]
66
+
67
+
68
+ def test_longer_period_and_float_input():
69
+ result = hg.eval_node(hga.pct_change, [10.0, 11.0, 12.0, 15.0], 2)
70
+ assert result[:2] == [None, None]
71
+ assert result[2:] == pytest.approx([0.2, 4.0 / 11.0])
72
+
73
+
74
+ @pytest.mark.parametrize(
75
+ ("policy", "expected"),
76
+ [
77
+ (hg.DivideByZero.NAN, math.nan),
78
+ (hg.DivideByZero.ZERO, 0.0),
79
+ (hg.DivideByZero.ONE, 1.0),
80
+ (hg.DivideByZero.NONE, None),
81
+ ],
82
+ )
83
+ def test_zero_denominator_policies(policy, expected):
84
+ result = hg.eval_node(hga.pct_change, [0.0, 1.0, 2.0], 1, policy)
85
+ assert result[0] is None
86
+ if math.isnan(expected) if isinstance(expected, float) else False:
87
+ assert math.isnan(result[1])
88
+ else:
89
+ assert result[1] == expected
90
+ assert result[2] == 1.0
91
+
92
+
93
+ def test_zero_denominator_error_is_the_default():
94
+ with pytest.raises(Exception, match="division by zero"):
95
+ hg.eval_node(hga.pct_change, [0.0, 1.0])
96
+
97
+
98
+ @pytest.mark.parametrize("period", [0, -1])
99
+ def test_period_must_be_positive(period):
100
+ with pytest.raises(hg.WiringError, match="period must be positive"):
101
+ hg.eval_node(hga.pct_change, [1.0, 2.0], period)
102
+
103
+
104
+ def test_public_signature_and_fractional_units():
105
+ assert hga.pct_change.__name__ == "pct_change"
106
+ assert hg.eval_node(hga.pct_change, [100.0, 105.0]) == [None, 0.05]
@@ -0,0 +1,148 @@
1
+ #include <hgraph/analytics/operators.h>
2
+
3
+ #include <hgraph/lib/std/operators/arithmetic.h>
4
+ #include <hgraph/lib/std/operators/stream.h>
5
+ #include <hgraph/types/graph_wiring.h>
6
+ #include <hgraph/types/value/value.h>
7
+
8
+ #include <algorithm>
9
+ #include <stdexcept>
10
+ #include <string_view>
11
+ #include <type_traits>
12
+ #include <utility>
13
+ #include <vector>
14
+
15
+ namespace hgraph::analytics
16
+ {
17
+ namespace
18
+ {
19
+ template <typename T>
20
+ struct diff_impl
21
+ {
22
+ static void eval(In<"ts", TS<T>> ts, RecordableState<TS<T>> prior,
23
+ Out<TS<T>> out)
24
+ {
25
+ // Adjacent difference is O(1) per tick and retains only the
26
+ // preceding valid observation. State is advanced after output
27
+ // so both operands belong to consecutive accepted observations.
28
+ if (prior.valid()) { out.set(ts.value() - prior.value().template checked_as<T>()); }
29
+ prior.set(ts.value());
30
+ }
31
+ };
32
+
33
+ struct count_impl
34
+ {
35
+ static void eval(In<"ts", SIGNAL> ts, State<Int> running_count,
36
+ Out<TS<Int>> out)
37
+ {
38
+ static_cast<void>(ts);
39
+ const Int next = running_count.get() + 1;
40
+ running_count.set(next);
41
+ out.set(next);
42
+ }
43
+ };
44
+
45
+ struct count_reset_impl
46
+ {
47
+ static void eval(In<"ts", SIGNAL, InputValidity::Unchecked> ts,
48
+ In<"reset", SIGNAL, InputValidity::Unchecked> reset,
49
+ State<Int> running_count, Out<TS<Int>> out)
50
+ {
51
+ // Reset wins on a shared cycle, then that cycle's source tick
52
+ // becomes observation one. A reset by itself intentionally does
53
+ // not tick the output.
54
+ if (reset.modified()) { running_count.set(Int{0}); }
55
+ if (!ts.modified()) { return; }
56
+ const Int next = running_count.get() + 1;
57
+ running_count.set(next);
58
+ out.set(next);
59
+ }
60
+ };
61
+
62
+ template <typename T>
63
+ struct clip_impl
64
+ {
65
+ static void start(Scalar<"min", T> minimum, Scalar<"max", T> maximum)
66
+ {
67
+ if (minimum.value() > maximum.value())
68
+ {
69
+ throw std::invalid_argument(
70
+ "hgraph.analytics.clip: min must be <= max");
71
+ }
72
+ }
73
+
74
+ static void eval(In<"ts", TS<T>> ts, Scalar<"min", T> minimum,
75
+ Scalar<"max", T> maximum, Out<TS<T>> out)
76
+ {
77
+ out.set(std::clamp(ts.value(), minimum.value(), maximum.value()));
78
+ }
79
+ };
80
+
81
+ struct ewma_impl
82
+ {
83
+ static void eval(In<"ts", TS<Float>> ts, Scalar<"alpha", Float> alpha,
84
+ RecordableState<TS<Float>> average, Out<TS<Float>> out)
85
+ {
86
+ // This is the migrated core recurrence, kept algebraically
87
+ // unchanged so the package move cannot alter floating-point
88
+ // rounding. It is O(1) per tick and initializes from the first
89
+ // observation.
90
+ const Float value = average.valid()
91
+ ? alpha.value() * ts.value() +
92
+ (Float{1.0} - alpha.value()) * average.value().checked_as<Float>()
93
+ : ts.value();
94
+ average.set(value);
95
+ out.set(value);
96
+ }
97
+ };
98
+
99
+ template <typename T>
100
+ struct pct_change_compose
101
+ {
102
+ static constexpr auto name = std::is_same_v<T, Int>
103
+ ? "hgraph.analytics.pct_change.int"
104
+ : "hgraph.analytics.pct_change.float";
105
+
106
+ static std::vector<std::pair<std::string_view, Value>> defaults()
107
+ {
108
+ return {
109
+ {"period", Value{Int{1}}},
110
+ {"divide_by_zero", Value{stdlib::DivideByZero::Error}},
111
+ };
112
+ }
113
+
114
+ static Port<TS<Float>> compose(
115
+ Wiring &w, NamedPort<"ts", TS<T>> ts,
116
+ Scalar<"period", Int> period,
117
+ Scalar<"divide_by_zero", stdlib::DivideByZero> divide_by_zero)
118
+ {
119
+ if (period.value() <= 0)
120
+ {
121
+ throw std::invalid_argument(
122
+ "hgraph.analytics.pct_change: period must be positive");
123
+ }
124
+
125
+ // The retained observation history and readiness contract stay
126
+ // in core lag. Reusing the same prior port for subtraction and
127
+ // division guarantees both operations observe one causal anchor.
128
+ auto prior = wire<stdlib::lag>(w, ts, period.value());
129
+ auto delta = wire<stdlib::sub_>(w, ts, prior);
130
+ return wire<stdlib::div_, TS<Float>>(
131
+ w, delta, prior, divide_by_zero.value());
132
+ }
133
+ };
134
+ } // namespace
135
+
136
+ void register_analytics_operators()
137
+ {
138
+ register_overload<diff, diff_impl<Int>>();
139
+ register_overload<diff, diff_impl<Float>>();
140
+ register_overload<count, count_impl>();
141
+ register_overload<count, count_reset_impl>();
142
+ register_overload<clip, clip_impl<Int>>();
143
+ register_overload<clip, clip_impl<Float>>();
144
+ register_overload<ewma, ewma_impl>();
145
+ register_graph_overload<pct_change, pct_change_compose<Int>>();
146
+ register_graph_overload<pct_change, pct_change_compose<Float>>();
147
+ }
148
+ } // namespace hgraph::analytics
@@ -0,0 +1,11 @@
1
+ #include <hgraph/analytics/operators.h>
2
+
3
+ #include <nanobind/nanobind.h>
4
+
5
+ namespace nb = nanobind;
6
+
7
+ NB_MODULE(_hgraph_analytics, module)
8
+ {
9
+ hgraph::analytics::register_analytics_operators();
10
+ module.doc() = "Native hgraph-analytics operator registration";
11
+ }
@@ -0,0 +1,24 @@
1
+ cmake_minimum_required(VERSION 3.25)
2
+
3
+ project(hgraph_analytics_consumer LANGUAGES CXX)
4
+
5
+ include(CTest)
6
+
7
+ find_package(hgraph-analytics CONFIG REQUIRED)
8
+
9
+ add_executable(hgraph_analytics_consumer main.cpp)
10
+ target_compile_features(hgraph_analytics_consumer PRIVATE cxx_std_23)
11
+ target_link_libraries(hgraph_analytics_consumer PRIVATE hgraph::analytics)
12
+
13
+ # A wheel-installed SDK exposes Python conversion hooks whose symbols are
14
+ # supplied by the host interpreter for extension modules. A standalone native
15
+ # executable using that SDK must embed Python; a Python-disabled native install
16
+ # exports no nanobind target and remains interpreter-free.
17
+ if(TARGET hgraph::nanobind AND NOT TARGET Python::Python)
18
+ find_package(Python 3.12 COMPONENTS Interpreter Development.Embed REQUIRED)
19
+ endif()
20
+ if(TARGET Python::Python)
21
+ target_link_libraries(hgraph_analytics_consumer PRIVATE Python::Python)
22
+ endif()
23
+
24
+ add_test(NAME hgraph_analytics_consumer COMMAND hgraph_analytics_consumer)
@@ -0,0 +1,40 @@
1
+ #include <hgraph/analytics/operators.h>
2
+
3
+ #include <hgraph/lib/std/operators/conversion.h>
4
+ #include <hgraph/lib/std/operators/registration.h>
5
+ #include <hgraph/types/graph_wiring.h>
6
+
7
+ namespace
8
+ {
9
+ namespace hg = hgraph;
10
+ namespace hga = hgraph::analytics;
11
+
12
+ struct InstalledConsumerGraph
13
+ {
14
+ static constexpr auto name = "installed_hgraph_analytics_consumer";
15
+
16
+ static void compose(hg::Wiring &w)
17
+ {
18
+ auto input = hg::wire<hg::stdlib::const_, hg::TS<hg::Float>>(
19
+ w, hg::Float{100.0});
20
+ auto reset = hg::wire<hg::stdlib::const_, hg::TS<hg::Bool>>(
21
+ w, hg::Bool{false});
22
+ static_cast<void>(hg::wire<hga::diff>(w, input));
23
+ static_cast<void>(hg::wire<hga::count>(w, input));
24
+ static_cast<void>(hg::wire<hga::count>(w, input, reset));
25
+ static_cast<void>(hg::wire<hga::clip>(
26
+ w, input, hg::Float{0.0}, hg::Float{200.0}));
27
+ static_cast<void>(hg::wire<hga::ewma>(w, input, hg::Float{0.2}));
28
+ static_cast<void>(hg::wire<hga::pct_change, hg::TS<hg::Float>>(
29
+ w, input, hg::Int{12}, hg::stdlib::DivideByZero::Nan));
30
+ }
31
+ };
32
+ } // namespace
33
+
34
+ int main()
35
+ {
36
+ hgraph::stdlib::register_standard_operators();
37
+ hgraph::analytics::register_analytics_operators();
38
+ auto graph = hgraph::build_graph<InstalledConsumerGraph>();
39
+ return graph.node_count() == 0 ? 1 : 0;
40
+ }
@@ -0,0 +1,17 @@
1
+ add_executable(hgraph_analytics_tests
2
+ test_pct_change.cpp
3
+ )
4
+
5
+ target_compile_features(hgraph_analytics_tests PRIVATE cxx_std_23)
6
+ target_link_libraries(hgraph_analytics_tests PRIVATE hgraph::analytics)
7
+
8
+ if(MSVC)
9
+ target_compile_options(hgraph_analytics_tests PRIVATE /W4 /permissive-)
10
+ else()
11
+ target_compile_options(hgraph_analytics_tests PRIVATE -Wall -Wextra -Wpedantic)
12
+ endif()
13
+
14
+ add_test(NAME hgraph_analytics_tests COMMAND hgraph_analytics_tests)
15
+ if(COMMAND hgraph_configure_test_runtime_paths)
16
+ hgraph_configure_test_runtime_paths(hgraph_analytics_tests)
17
+ endif()
@@ -0,0 +1,256 @@
1
+ #include <hgraph/analytics/operators.h>
2
+
3
+ #include <hgraph/lib/std/operators/registration.h>
4
+ #include <hgraph/lib/testing/eval_node.h>
5
+
6
+ #include <cmath>
7
+ #include <cstddef>
8
+ #include <iostream>
9
+ #include <optional>
10
+ #include <stdexcept>
11
+ #include <string>
12
+ #include <type_traits>
13
+ #include <utility>
14
+ #include <vector>
15
+
16
+ namespace
17
+ {
18
+ using namespace hgraph;
19
+ using namespace hgraph::analytics;
20
+ using namespace hgraph::testing;
21
+
22
+ inline constexpr std::nullopt_t none = std::nullopt;
23
+
24
+ template <typename T, typename U>
25
+ [[nodiscard]] std::optional<T> optional_value(U &&value)
26
+ {
27
+ return T{std::forward<U>(value)};
28
+ }
29
+
30
+ template <typename T>
31
+ [[nodiscard]] std::optional<T> optional_value(std::nullopt_t)
32
+ {
33
+ return std::nullopt;
34
+ }
35
+
36
+ template <typename T, typename... Args>
37
+ [[nodiscard]] std::vector<std::optional<T>> values(Args &&...args)
38
+ {
39
+ std::vector<std::optional<T>> output;
40
+ output.reserve(sizeof...(Args));
41
+ (output.push_back(optional_value<T>(std::forward<Args>(args))), ...);
42
+ return output;
43
+ }
44
+
45
+ void require(bool condition, std::string message)
46
+ {
47
+ if (!condition) { throw std::runtime_error(std::move(message)); }
48
+ }
49
+
50
+ void require_output(
51
+ const std::vector<std::optional<Float>> &actual,
52
+ const std::vector<std::optional<Float>> &expected,
53
+ const std::string &label)
54
+ {
55
+ require(actual.size() == expected.size(), label + ": output size");
56
+ for (std::size_t index = 0; index < expected.size(); ++index)
57
+ {
58
+ require(actual[index].has_value() == expected[index].has_value(),
59
+ label + ": readiness at index " + std::to_string(index));
60
+ if (expected[index].has_value())
61
+ {
62
+ require(std::abs(*actual[index] - *expected[index]) <= 1.0e-12,
63
+ label + ": value at index " + std::to_string(index));
64
+ }
65
+ }
66
+ }
67
+
68
+ template <typename T>
69
+ void require_value_output(
70
+ const std::vector<std::optional<Value>> &actual,
71
+ const std::vector<std::optional<T>> &expected,
72
+ const std::string &label)
73
+ {
74
+ require(actual.size() == expected.size(), label + ": output size");
75
+ for (std::size_t index = 0; index < expected.size(); ++index)
76
+ {
77
+ require(actual[index].has_value() == expected[index].has_value(),
78
+ label + ": readiness at index " + std::to_string(index));
79
+ if (!expected[index].has_value()) { continue; }
80
+ const T value = actual[index]->view().checked_as<T>();
81
+ if constexpr (std::is_floating_point_v<T>)
82
+ {
83
+ require(std::abs(value - *expected[index]) <= 1.0e-12,
84
+ label + ": value at index " + std::to_string(index));
85
+ }
86
+ else
87
+ {
88
+ require(value == *expected[index],
89
+ label + ": value at index " + std::to_string(index));
90
+ }
91
+ }
92
+ }
93
+
94
+ struct DefaultIntChange
95
+ {
96
+ static constexpr auto name = "analytics_default_int_change";
97
+
98
+ static Port<TS<Float>> compose(Wiring &w, Port<TS<Int>> ts)
99
+ {
100
+ return wire<pct_change, TS<Float>>(w, ts);
101
+ }
102
+ };
103
+
104
+ struct TwoPeriodFloatChange
105
+ {
106
+ static constexpr auto name = "analytics_two_period_float_change";
107
+
108
+ static Port<TS<Float>> compose(Wiring &w, Port<TS<Float>> ts)
109
+ {
110
+ return wire<pct_change, TS<Float>>(w, ts, Int{2});
111
+ }
112
+ };
113
+
114
+ template <stdlib::DivideByZero Policy>
115
+ struct ZeroPolicyChange
116
+ {
117
+ static constexpr auto name = "analytics_zero_policy_change";
118
+
119
+ static Port<TS<Float>> compose(Wiring &w, Port<TS<Float>> ts)
120
+ {
121
+ return wire<pct_change, TS<Float>>(w, ts, Int{1}, Policy);
122
+ }
123
+ };
124
+
125
+ struct InvalidPeriodChange
126
+ {
127
+ static constexpr auto name = "analytics_invalid_period_change";
128
+
129
+ static Port<TS<Float>> compose(Wiring &w, Port<TS<Float>> ts)
130
+ {
131
+ return wire<pct_change, TS<Float>>(w, ts, Int{0});
132
+ }
133
+ };
134
+
135
+ void test_default_and_sparse_observations()
136
+ {
137
+ require_output(eval_node<DefaultIntChange>(values<Int>(1, 2, 3)),
138
+ {std::nullopt, 1.0, 0.5}, "default period");
139
+
140
+ require_output(eval_node<DefaultIntChange>(values<Int>(1, none, 2)),
141
+ {std::nullopt, std::nullopt, 1.0}, "sparse observations");
142
+ }
143
+
144
+ void test_period_and_float_input()
145
+ {
146
+ require_output(
147
+ eval_node<TwoPeriodFloatChange>(
148
+ values<Float>(10.0, 11.0, 12.0, 15.0)),
149
+ {std::nullopt, std::nullopt, 0.2, 4.0 / 11.0},
150
+ "two-period change");
151
+ }
152
+
153
+ void test_zero_policies()
154
+ {
155
+ const auto nan = eval_node<ZeroPolicyChange<stdlib::DivideByZero::Nan>>(
156
+ values<Float>(0.0, 1.0));
157
+ require(nan.size() == 2 && !nan[0].has_value() && nan[1].has_value(),
158
+ "NaN policy output readiness");
159
+ require(std::isnan(*nan[1]),
160
+ "NaN policy result");
161
+
162
+ require_output(
163
+ eval_node<ZeroPolicyChange<stdlib::DivideByZero::Zero>>(
164
+ values<Float>(0.0, 1.0)),
165
+ {std::nullopt, 0.0}, "zero policy");
166
+ require_output(
167
+ eval_node<ZeroPolicyChange<stdlib::DivideByZero::One>>(
168
+ values<Float>(0.0, 1.0)),
169
+ {std::nullopt, 1.0}, "one policy");
170
+ require_output(
171
+ eval_node<ZeroPolicyChange<stdlib::DivideByZero::NoTick>>(
172
+ values<Float>(0.0, 1.0)),
173
+ {std::nullopt, std::nullopt}, "no-tick policy");
174
+
175
+ bool raised = false;
176
+ try
177
+ {
178
+ static_cast<void>(
179
+ eval_node<ZeroPolicyChange<stdlib::DivideByZero::Error>>(
180
+ values<Float>(0.0, 1.0)));
181
+ }
182
+ catch (const std::exception &)
183
+ {
184
+ raised = true;
185
+ }
186
+ require(raised, "Error policy rejects a zero prior value");
187
+ }
188
+
189
+ void test_invalid_period()
190
+ {
191
+ bool raised = false;
192
+ try
193
+ {
194
+ static_cast<void>(eval_node<InvalidPeriodChange>(values<Float>(1.0)));
195
+ }
196
+ catch (const std::invalid_argument &)
197
+ {
198
+ raised = true;
199
+ }
200
+ require(raised, "non-positive period is rejected while wiring");
201
+ }
202
+
203
+ void test_migrated_analytical_operators()
204
+ {
205
+ require_value_output(eval_node<diff>(values<Int>(1, 2, 4, 7)),
206
+ values<Int>(none, 1, 2, 3), "integer diff");
207
+ require_value_output(eval_node<diff>(values<Float>(1.0, 1.5, 3.0)),
208
+ values<Float>(none, 0.5, 1.5), "floating diff");
209
+ require_value_output(eval_node<count>(values<Int>(3, none, 2, 1)),
210
+ values<Int>(1, none, 2, 3), "count");
211
+ require_value_output(eval_node<count>(values<Int>(3, 2, 1),
212
+ values<Bool>(none, true, none)),
213
+ values<Int>(1, 1, 2), "reset count");
214
+ require_value_output(eval_node<clip>(values<Int>(-1, 1, 3),
215
+ Int{0}, Int{2}),
216
+ values<Int>(0, 1, 2), "integer clip");
217
+ require_value_output(eval_node<clip>(values<Float>(-1.0, 0.5, 2.0),
218
+ Float{0.0}, Float{1.0}),
219
+ values<Float>(0.0, 0.5, 1.0), "floating clip");
220
+ require_value_output(eval_node<ewma>(values<Float>(1.0, 2.0, 3.0, 4.0),
221
+ Float{0.5}),
222
+ values<Float>(1.0, 1.5, 2.25, 3.125), "ewma");
223
+
224
+ bool raised = false;
225
+ try
226
+ {
227
+ static_cast<void>(eval_node<clip>(
228
+ values<Float>(1.0), Float{1.0}, Float{-1.0}));
229
+ }
230
+ catch (const std::exception &)
231
+ {
232
+ raised = true;
233
+ }
234
+ require(raised, "clip rejects reversed bounds");
235
+ }
236
+ } // namespace
237
+
238
+ int main()
239
+ {
240
+ try
241
+ {
242
+ hgraph::stdlib::register_standard_operators();
243
+ hgraph::analytics::register_analytics_operators();
244
+ test_default_and_sparse_observations();
245
+ test_period_and_float_input();
246
+ test_zero_policies();
247
+ test_invalid_period();
248
+ test_migrated_analytical_operators();
249
+ return 0;
250
+ }
251
+ catch (const std::exception &error)
252
+ {
253
+ std::cerr << error.what() << '\n';
254
+ return 1;
255
+ }
256
+ }
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env python3
2
+ """Audit hgraph-analytics wheels and source distributions."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import argparse
7
+ import glob
8
+ import itertools
9
+ import tarfile
10
+ import zipfile
11
+ from pathlib import Path
12
+
13
+ WHEEL_REQUIRED = (
14
+ "hgraph_analytics/__init__.py",
15
+ "hgraph_analytics/py.typed",
16
+ "include/hgraph/analytics/operators.h",
17
+ # GNUInstallDirs selects lib64 on manylinux, while macOS and Windows wheels
18
+ # use lib. Match the package-relative suffix so both layouts are audited.
19
+ "cmake/hgraph-analytics/hgraph-analyticsConfig.cmake",
20
+ "cmake/hgraph-analytics/hgraphAnalyticsTargets.cmake",
21
+ )
22
+ SDIST_REQUIRED = (
23
+ "CMakeLists.txt",
24
+ "cmake/hgraph-analyticsConfig.cmake.in",
25
+ "include/hgraph/analytics/operators.h",
26
+ "python/hgraph_analytics/__init__.py",
27
+ "python/hgraph_analytics/py.typed",
28
+ "src/operators.cpp",
29
+ "test_package/CMakeLists.txt",
30
+ "tests/test_pct_change.cpp",
31
+ "tools/audit_distribution.py",
32
+ )
33
+
34
+
35
+ def _assert_required(names: set[str], required: tuple[str, ...], path: Path) -> None:
36
+ for item in required:
37
+ if not any(name.endswith(f"/{item}") or name == item for name in names):
38
+ raise AssertionError(f"{path}: missing {item}")
39
+
40
+
41
+ def _audit_wheel(path: Path) -> None:
42
+ with zipfile.ZipFile(path) as archive:
43
+ names = set(archive.namelist())
44
+ metadata_names = [name for name in names if name.endswith(".dist-info/METADATA")]
45
+ if len(metadata_names) != 1:
46
+ raise AssertionError(f"{path}: expected one METADATA file, got {metadata_names}")
47
+ metadata = archive.read(metadata_names[0]).decode()
48
+
49
+ _assert_required(names, WHEEL_REQUIRED, path)
50
+ native_modules = [
51
+ name
52
+ for name in names
53
+ if name.startswith("hgraph_analytics/_hgraph_analytics.")
54
+ and name.endswith((".so", ".pyd"))
55
+ ]
56
+ if len(native_modules) != 1:
57
+ raise AssertionError(
58
+ f"{path}: expected one native hgraph_analytics module, got {native_modules}"
59
+ )
60
+ analytics_libraries = [
61
+ name
62
+ for name in names
63
+ if Path(name).name.startswith(("libhgraph_analytics", "hgraph_analytics"))
64
+ and Path(name).suffix in {".a", ".lib"}
65
+ ]
66
+ if len(analytics_libraries) != 1:
67
+ raise AssertionError(
68
+ f"{path}: expected one native analytics library, got {analytics_libraries}"
69
+ )
70
+ forbidden = [
71
+ name
72
+ for name in names
73
+ if Path(name).name.startswith(
74
+ (
75
+ "hgraph_runtime",
76
+ "hgraph_stdlib",
77
+ "hgraph_wiring",
78
+ "libhgraph_runtime",
79
+ "libhgraph_stdlib",
80
+ "libhgraph_wiring",
81
+ "libnanobind-abi3",
82
+ "nanobind-abi3",
83
+ )
84
+ )
85
+ ]
86
+ if forbidden:
87
+ raise AssertionError(f"{path}: embeds core runtime libraries: {forbidden}")
88
+ if "Requires-Dist: hgraph" not in metadata:
89
+ raise AssertionError(f"{path}: does not declare the core distribution dependency")
90
+
91
+
92
+ def _audit_sdist(path: Path) -> None:
93
+ with tarfile.open(path, "r:gz") as archive:
94
+ names = set(archive.getnames())
95
+ _assert_required(names, SDIST_REQUIRED, path)
96
+
97
+
98
+ def _audit_distribution(path: Path) -> None:
99
+ if path.suffix == ".whl":
100
+ _audit_wheel(path)
101
+ elif path.name.endswith(".tar.gz"):
102
+ _audit_sdist(path)
103
+ else:
104
+ raise SystemExit(f"unsupported distribution: {path}")
105
+ print(f"audited {path}")
106
+
107
+
108
+ def main() -> None:
109
+ arguments = argparse.ArgumentParser()
110
+ arguments.add_argument("distributions", nargs="+")
111
+ patterns = arguments.parse_args().distributions
112
+ matches = itertools.chain.from_iterable(glob.iglob(pattern) for pattern in patterns)
113
+ paths = sorted(map(Path, set(matches)))
114
+ if not paths:
115
+ raise SystemExit("no hgraph-analytics distributions matched")
116
+ for distribution in paths:
117
+ _audit_distribution(distribution)
118
+
119
+
120
+ if __name__ == "__main__":
121
+ main()