From 436f41e778e4fecd7256459e05385abb9c63733c Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 20 May 2026 16:25:55 +0200 Subject: [PATCH 1/3] fix: update README for accuracy in dependencies and function descriptions --- README.md | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2452ef0..6c4e503 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ **Reverse-mode second-order automatic differentiation for Python.** -HyperGraph computes exact **gradients** and **Hessians** of scalar expressions using a computational-graph approach based on reverse-mode automatic differentiation. The core is implemented in C++ with [Eigen](https://eigen.tuxfamily.org/) and exposed to Python via [pybind11](https://github.com/pybind/pybind11), making it both fast and easy to use. +HyperGraph computes exact **gradients** and **Hessians** of scalar expressions using a computational-graph approach based on reverse-mode automatic differentiation. The core is implemented in C++ with [Eigen](https://eigen.tuxfamily.org/) and exposed to Python via [nanobind](https://github.com/wjakob/nanobind), making it both fast and easy to use. [![CI](https://github.com/oberbichler/HyperGraph/actions/workflows/ci.yml/badge.svg)](https://github.com/oberbichler/HyperGraph/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/hypergraph)](https://pypi.org/project/hypergraph) @@ -17,9 +17,9 @@ HyperGraph computes exact **gradients** and **Hessians** of scalar expressions u ## Features - **Second-order derivatives** — computes both the gradient vector and the full Hessian matrix in a single pass -- **High performance** — C++17 core with Eigen, optional BLAS acceleration +- **High performance** — C++17 core with Eigen 5, optional BLAS acceleration - **NumPy integration** — HyperGraph variables work seamlessly with `np.sin`, `np.cos`, `np.sqrt`, `np.dot`, `np.cross`, `np.linalg.norm`, and more -- **Comprehensive math library** — arithmetic, trigonometric, hyperbolic, inverse-hyperbolic, exponential, logarithmic, and power functions +- **Comprehensive math library** — arithmetic, trigonometric, hyperbolic, inverse-hyperbolic, exponential, logarithmic, power, error, and activation functions - **Cross-platform** — tested on Linux, macOS, and Windows with Python 3.12 and 3.13 ## Installation @@ -112,14 +112,38 @@ graph.h(out=h_out) | **Comparison** | `==`, `!=`, `<`, `>`, `<=`, `>=` | | **Trigonometric** | `sin`, `cos`, `tan`, `arcsin`, `arccos`, `arctan`, `atan2` | | **Hyperbolic** | `sinh`, `cosh`, `tanh`, `arcsinh`, `arccosh`, `arctanh` | -| **Exponential** | `exp`, `log` | -| **Power / Root** | `pow` (`**`), `square`, `sqrt`, `abs` | +| **Exponential / Logarithmic** | `exp`, `log`, `log2`, `log10` | +| **Power / Root** | `pow` (`**`), `pow(x, y)`, `square`, `sqrt`, `abs`, `hypot` | +| **Error functions** | `erf`, `erfc` | +| **Activation functions** | `sigmoid`, `softplus` | +| **Min / Max** | `min`, `max` | -Trigonometric and root functions can be called via NumPy (`np.sin(x)`, `np.sqrt(x)`, …) or as methods on a variable (`x.sin()`, `x.sqrt()`, …). The `atan2` function is available as `hg.atan2(y, x)`. +Most functions can be called via NumPy (`np.sin(x)`, `np.sqrt(x)`, …) or as methods on a variable (`x.sin()`, `x.sqrt()`, …). + +Two-argument functions are available as module-level functions: + +```python +hg.atan2(y, x) # arctangent of y/x +hg.pow(x, y) # x raised to the power y (both variables) +hg.hypot(x, y) # sqrt(x² + y²) +hg.min(x, y) # minimum of x and y +hg.max(x, y) # maximum of x and y +``` + +Additional module-level functions: + +```python +hg.log2(x) # base-2 logarithm +hg.log10(x) # base-10 logarithm +hg.erf(x) # error function +hg.erfc(x) # complementary error function +hg.sigmoid(x) # sigmoid: 1 / (1 + exp(-x)) +hg.softplus(x) # softplus: log(1 + exp(x)) +``` ## Building from Source -HyperGraph uses [scikit-build-core](https://github.com/scikit-build/scikit-build-core) and requires a C++17 compiler, CMake ≥ 3.24, and pybind11. [Eigen 3.4](https://eigen.tuxfamily.org/) is fetched automatically during the build. +HyperGraph uses [scikit-build-core](https://github.com/scikit-build/scikit-build-core) and requires a C++17 compiler, CMake ≥ 3.24, and [nanobind](https://github.com/wjakob/nanobind). [Eigen 5.0](https://eigen.tuxfamily.org/) is fetched automatically during the build. ```bash # Clone the repository From e565a2e36c50bf900dbbe6789d032a95eaf78dd2 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 20 May 2026 17:03:24 +0200 Subject: [PATCH 2/3] feat: implement to_dot method for computation graph visualization and add corresponding tests --- include/hypergraph/hypergraph.h | 176 +++++++++++++++++++++++++------- tests/TestHyperGraph.py | 131 ++++++++++++++++++++++++ 2 files changed, 268 insertions(+), 39 deletions(-) diff --git a/include/hypergraph/hypergraph.h b/include/hypergraph/hypergraph.h index 1a7f8d9..276d17b 100644 --- a/include/hypergraph/hypergraph.h +++ b/include/hypergraph/hypergraph.h @@ -19,7 +19,9 @@ #include #include #include +#include #include +#include #include namespace hypergraph { @@ -397,13 +399,15 @@ class HyperGraph { std::vector> m_vertices; std::vector> m_second_order_edges; std::vector m_self_second_order_edges; + std::vector m_vertex_labels; public: // constructor public: // methods - Variable new_tmp_variable(const T value) + Variable new_tmp_variable(const T value, const std::string& label = "") { const index id = length(m_vertices); m_vertices.emplace_back(id); + m_vertex_labels.push_back(label); return Variable(this, value, id); } @@ -411,6 +415,7 @@ class HyperGraph { { const index id = length(m_vertices); m_vertices.emplace_back(id); + m_vertex_labels.push_back("x" + std::to_string(length(m_variables))); return m_variables.emplace_back(this, value, id); } @@ -733,6 +738,70 @@ class HyperGraph { return {std::move(rows), std::move(cols), std::move(values)}; } + std::string to_dot() const + { + std::ostringstream ss; + ss << "digraph ComputationGraph {\n"; + ss << " rankdir=BT;\n"; + ss << " node [shape=record, style=filled];\n\n"; + + const index n_vertices = length(m_vertices); + + // Build a set of variable vertex IDs for styling + tsl::robin_map var_ids; + for (index i = 0; i < length(m_variables); i++) { + var_ids[m_variables[i].id()] = i; + } + + // Emit nodes + for (index vid = 0; vid < n_vertices; vid++) { + const auto& v = m_vertices[vid]; + const std::string& label = m_vertex_labels[vid]; + const bool is_var = var_ids.find(vid) != var_ids.end(); + + ss << " v" << vid << " [label=\""; + if (!label.empty()) { + ss << label; + } else { + ss << "v" << vid; + } + ss << "\""; + + if (is_var) { + ss << ", fillcolor=\"#a8d8ea\", shape=ellipse"; + } else { + ss << ", fillcolor=\"#f8f8f8\""; + } + + ss << "];\n"; + } + + ss << "\n"; + + // Emit edges (from parent to child = data flow direction) + for (index vid = 0; vid < n_vertices; vid++) { + const auto& v = m_vertices[vid]; + const auto& e1 = v.edge1(); + const auto& e2 = v.edge2(); + + if (e1.to() != vid) { + ss << " v" << e1.to() << " -> v" << vid + << " [label=\"" << e1.weight() << "\"];\n"; + } + if (e2.to() != vid && e2.to() != e1.to()) { + ss << " v" << e2.to() << " -> v" << vid + << " [label=\"" << e2.weight() << "\"];\n"; + } else if (e2.to() != vid && e2.to() == e1.to()) { + // Same parent appears twice (e.g. x*x); show both edges + ss << " v" << e2.to() << " -> v" << vid + << " [label=\"" << e2.weight() << "\"];\n"; + } + } + + ss << "}\n"; + return ss.str(); + } + public: // python template static void register_python(TModule& m) @@ -784,7 +853,36 @@ class HyperGraph { } else { throw std::invalid_argument("h_sparse: format must be 'coo', 'csc', or 'csr'"); } - }, "format"_a = "csc", "full"_a = false); + }, "format"_a = "csc", "full"_a = false) + .def("to_dot", [](const Type& self, const std::string& path) -> nb::object { + const std::string dot = self.to_dot(); + + if (path.empty()) { + return nb::cast(dot); + } + + // If a path is given, try to render via graphviz + nb::module_ subprocess = nb::module_::import_("subprocess"); + nb::module_ builtins = nb::module_::import_("builtins"); + + // Determine format from extension + std::string fmt = "svg"; + auto dot_pos = path.rfind('.'); + if (dot_pos != std::string::npos) { + fmt = path.substr(dot_pos + 1); + } + + // Write the DOT string to the output file via `dot` command + nb::object result = subprocess.attr("run")( + nb::make_tuple("dot", "-T" + fmt, "-o", path), + "input"_a = dot, + "capture_output"_a = true, + "text"_a = true, + "check"_a = true + ); + + return nb::cast(dot); + }, "path"_a = ""); } }; @@ -901,7 +999,7 @@ HYPERGRAPH_INLINE Variable operator-(const Variable& x) { HyperGraph* graph = x.graph(); - const Variable result = graph->new_tmp_variable(-x.value()); + const Variable result = graph->new_tmp_variable(-x.value(), "neg"); graph->add_edge(result, x, -1.0, 0.0); return result; } @@ -911,7 +1009,7 @@ HYPERGRAPH_INLINE Variable operator+(const Variable& lhs, const Variable* graph = lhs.graph(); - const Variable result = graph->new_tmp_variable(lhs.value() + rhs.value()); + const Variable result = graph->new_tmp_variable(lhs.value() + rhs.value(), "+"); graph->add_edge(result, lhs, rhs, 1.0, 1.0, 0.0); return result; } @@ -921,7 +1019,7 @@ HYPERGRAPH_INLINE Variable operator+(const Variable& lhs, const double rhs { HyperGraph* graph = lhs.graph(); - const Variable result = graph->new_tmp_variable(lhs.value() + rhs); + const Variable result = graph->new_tmp_variable(lhs.value() + rhs, "+"); graph->add_edge(result, lhs, 1.0, 0.0); return result; } @@ -951,7 +1049,7 @@ HYPERGRAPH_INLINE Variable operator-(const Variable& lhs, const Variable* graph = lhs.graph(); - const Variable result = graph->new_tmp_variable(lhs.value() - rhs.value()); + const Variable result = graph->new_tmp_variable(lhs.value() - rhs.value(), "-"); graph->add_edge(result, lhs, rhs, 1.0, -1.0, 0.0); return result; } @@ -961,7 +1059,7 @@ HYPERGRAPH_INLINE Variable operator-(const Variable& lhs, const double rhs { HyperGraph* graph = lhs.graph(); - const Variable result = graph->new_tmp_variable(lhs.value() - rhs); + const Variable result = graph->new_tmp_variable(lhs.value() - rhs, "-"); graph->add_edge(result, lhs, 1.0, 0.0); return result; } @@ -971,7 +1069,7 @@ HYPERGRAPH_INLINE Variable operator-(const double lhs, const Variable& rhs { HyperGraph* graph = rhs.graph(); - const Variable result = graph->new_tmp_variable(lhs - rhs.value()); + const Variable result = graph->new_tmp_variable(lhs - rhs.value(), "-"); graph->add_edge(result, rhs, -1.0, 0.0); return result; } @@ -995,7 +1093,7 @@ HYPERGRAPH_INLINE Variable operator*(const Variable& lhs, const Variable* graph = lhs.graph(); - const Variable result = graph->new_tmp_variable(lhs.value() * rhs.value()); + const Variable result = graph->new_tmp_variable(lhs.value() * rhs.value(), "*"); graph->add_edge(result, lhs, rhs, rhs.value(), lhs.value(), 1.0); return result; } @@ -1005,7 +1103,7 @@ HYPERGRAPH_INLINE Variable operator*(const Variable& lhs, const double rhs { HyperGraph* graph = lhs.graph(); - const Variable result = graph->new_tmp_variable(lhs.value() * rhs); + const Variable result = graph->new_tmp_variable(lhs.value() * rhs, "*"); graph->add_edge(result, lhs, rhs, 0.0); return result; } @@ -1044,7 +1142,7 @@ HYPERGRAPH_INLINE Variable inv(const Variable& x) const auto inv_x = 1.0 / x.value(); const auto inv_x_sq = inv_x * inv_x; const auto inv_x_cu = inv_x_sq * inv_x; - const Variable result = graph->new_tmp_variable(inv_x); + const Variable result = graph->new_tmp_variable(inv_x, "inv"); graph->add_edge(result, x, -inv_x_sq, 2.0 * inv_x_cu); return result; } @@ -1116,7 +1214,7 @@ HYPERGRAPH_INLINE Variable abs(const Variable& x) } else { // Subgradient convention: abs(0) = 0, d|x|/dx = 0 at x = 0 HyperGraph* graph = x.graph(); - const Variable result = graph->new_tmp_variable(0.0); + const Variable result = graph->new_tmp_variable(0.0, "abs"); graph->add_edge(result, x, 0.0, 0.0); return result; } @@ -1128,7 +1226,7 @@ HYPERGRAPH_INLINE Variable square(const Variable& x) HyperGraph* graph = x.graph(); const auto sq = x.value() * x.value(); - const Variable result = graph->new_tmp_variable(sq); + const Variable result = graph->new_tmp_variable(sq, "square"); graph->add_edge(result, x, 2.0 * x.value(), 2.0); return result; } @@ -1151,7 +1249,7 @@ HYPERGRAPH_INLINE Variable sqrt(const Variable& x) const auto sqrt_x = sqrt(x.value()); const auto inv_sqrt = 1.0 / sqrt_x; - const Variable result = graph->new_tmp_variable(sqrt_x); + const Variable result = graph->new_tmp_variable(sqrt_x, "sqrt"); graph->add_edge(result, x, 0.5 * inv_sqrt, -0.25 * inv_sqrt / x.value()); return result; } @@ -1170,7 +1268,7 @@ HYPERGRAPH_INLINE Variable pow(const Variable& x, const double a) HyperGraph* graph = x.graph(); const auto pow_x = pow(x.value(), a); - const Variable result = graph->new_tmp_variable(pow_x); + const Variable result = graph->new_tmp_variable(pow_x, "pow"); graph->add_edge(result, x, a * pow(x.value(), a - 1.0), a * (a - 1.0) * pow(x.value(), a - 2.0)); return result; } @@ -1183,7 +1281,7 @@ HYPERGRAPH_INLINE Variable exp(const Variable& x) HyperGraph* graph = x.graph(); const auto exp_x = exp(x.value()); - const Variable result = graph->new_tmp_variable(exp_x); + const Variable result = graph->new_tmp_variable(exp_x, "exp"); graph->add_edge(result, x, exp_x, exp_x); return result; } @@ -1202,7 +1300,7 @@ HYPERGRAPH_INLINE Variable log(const Variable& x) HyperGraph* graph = x.graph(); const auto log_x = log(x.value()); - const Variable result = graph->new_tmp_variable(log_x); + const Variable result = graph->new_tmp_variable(log_x, "log"); const auto inv = 1.0 / x.value(); graph->add_edge(result, x, inv, -inv * inv); return result; @@ -1217,7 +1315,7 @@ HYPERGRAPH_INLINE Variable cos(const Variable& x) HyperGraph* graph = x.graph(); const auto cos_x = cos(x.value()); - const Variable result = graph->new_tmp_variable(cos_x); + const Variable result = graph->new_tmp_variable(cos_x, "cos"); graph->add_edge(result, x, -sin(x.value()), -cos_x); return result; } @@ -1231,7 +1329,7 @@ HYPERGRAPH_INLINE Variable sin(const Variable& x) HyperGraph* graph = x.graph(); const auto sin_x = sin(x.value()); - const Variable result = graph->new_tmp_variable(sin_x); + const Variable result = graph->new_tmp_variable(sin_x, "sin"); graph->add_edge(result, x, cos(x.value()), -sin_x); return result; } @@ -1254,7 +1352,7 @@ HYPERGRAPH_INLINE Variable tan(const Variable& x) const auto tan_x = tan(x.value()); const auto sec = 1.0 / cos(x.value()); const auto sec_sq = sec * sec; - const Variable result = graph->new_tmp_variable(tan_x); + const Variable result = graph->new_tmp_variable(tan_x, "tan"); graph->add_edge(result, x, sec_sq, 2.0 * tan_x * sec_sq); return result; } @@ -1274,7 +1372,7 @@ HYPERGRAPH_INLINE Variable acos(const Variable& x) HyperGraph* graph = x.graph(); const auto acos_x = acos(x.value()); - const Variable result = graph->new_tmp_variable(acos_x); + const Variable result = graph->new_tmp_variable(acos_x, "acos"); const auto tmp = 1.0 / (1.0 - x.value() * x.value()); const auto neg_tmp_sqrt = -sqrt(tmp); graph->add_edge(result, x, neg_tmp_sqrt, x.value() * neg_tmp_sqrt * tmp); @@ -1296,7 +1394,7 @@ HYPERGRAPH_INLINE Variable asin(const Variable& x) HyperGraph* graph = x.graph(); const auto asin_x = asin(x.value()); - const Variable result = graph->new_tmp_variable(asin_x); + const Variable result = graph->new_tmp_variable(asin_x, "asin"); const auto tmp = 1.0 / (1.0 - x.value() * x.value()); const auto tmp_sqrt = sqrt(tmp); graph->add_edge(result, x, tmp_sqrt, x.value() * tmp_sqrt * tmp); @@ -1311,7 +1409,7 @@ HYPERGRAPH_INLINE Variable atan(const Variable& x) HyperGraph* graph = x.graph(); const auto atan_x = atan(x.value()); - const Variable result = graph->new_tmp_variable(atan_x); + const Variable result = graph->new_tmp_variable(atan_x, "atan"); const auto tmp = 1 / (1 + x.value() * x.value()); graph->add_edge(result, x, tmp, -2 * x.value() * tmp * tmp); return result; @@ -1326,7 +1424,7 @@ HYPERGRAPH_INLINE Variable sinh(const Variable& x) HyperGraph* graph = x.graph(); const auto sinh_x = sinh(x.value()); - const Variable result = graph->new_tmp_variable(sinh_x); + const Variable result = graph->new_tmp_variable(sinh_x, "sinh"); graph->add_edge(result, x, cosh(x.value()), sinh_x); return result; } @@ -1340,7 +1438,7 @@ HYPERGRAPH_INLINE Variable cosh(const Variable& x) HyperGraph* graph = x.graph(); const auto cosh_x = cosh(x.value()); - const Variable result = graph->new_tmp_variable(cosh_x); + const Variable result = graph->new_tmp_variable(cosh_x, "cosh"); graph->add_edge(result, x, sinh(x.value()), cosh_x); return result; } @@ -1354,7 +1452,7 @@ HYPERGRAPH_INLINE Variable tanh(const Variable& x) const auto tanh_x = tanh(x.value()); const auto sech_sq = 1.0 - tanh_x * tanh_x; - const Variable result = graph->new_tmp_variable(tanh_x); + const Variable result = graph->new_tmp_variable(tanh_x, "tanh"); graph->add_edge(result, x, sech_sq, -2.0 * tanh_x * sech_sq); return result; } @@ -1370,7 +1468,7 @@ HYPERGRAPH_INLINE Variable asinh(const Variable& x) const auto asinh_x = asinh(x.value()); const auto tmp = 1.0 / (1.0 + x.value() * x.value()); const auto tmp_sqrt = sqrt(tmp); - const Variable result = graph->new_tmp_variable(asinh_x); + const Variable result = graph->new_tmp_variable(asinh_x, "asinh"); graph->add_edge(result, x, tmp_sqrt, -x.value() * tmp_sqrt * tmp); return result; } @@ -1392,7 +1490,7 @@ HYPERGRAPH_INLINE Variable acosh(const Variable& x) const auto acosh_x = acosh(x.value()); const auto tmp = 1.0 / (x.value() * x.value() - 1.0); const auto tmp_sqrt = sqrt(tmp); - const Variable result = graph->new_tmp_variable(acosh_x); + const Variable result = graph->new_tmp_variable(acosh_x, "acosh"); graph->add_edge(result, x, tmp_sqrt, -x.value() * tmp_sqrt * tmp); return result; } @@ -1413,7 +1511,7 @@ HYPERGRAPH_INLINE Variable atanh(const Variable& x) const auto atanh_x = atanh(x.value()); const auto one_minus_x2 = 1.0 - x.value() * x.value(); const auto tmp = 1.0 / one_minus_x2; - const Variable result = graph->new_tmp_variable(atanh_x); + const Variable result = graph->new_tmp_variable(atanh_x, "atanh"); graph->add_edge(result, x, tmp, 2.0 * x.value() * tmp * tmp); return result; } @@ -1460,7 +1558,7 @@ HYPERGRAPH_INLINE Variable log2(const Variable& x) const auto inv = 1.0 / x.value(); const auto ln2 = log(2.0); const auto inv_ln2 = 1.0 / ln2; - const Variable result = graph->new_tmp_variable(log2_x); + const Variable result = graph->new_tmp_variable(log2_x, "log2"); // d(log2(x))/dx = 1/(x·ln2), d²(log2(x))/dx² = -1/(x²·ln2) graph->add_edge(result, x, inv * inv_ln2, -inv * inv * inv_ln2); return result; @@ -1484,7 +1582,7 @@ HYPERGRAPH_INLINE Variable log10(const Variable& x) const auto inv = 1.0 / x.value(); const auto ln10 = log(10.0); const auto inv_ln10 = 1.0 / ln10; - const Variable result = graph->new_tmp_variable(log10_x); + const Variable result = graph->new_tmp_variable(log10_x, "log10"); // d(log10(x))/dx = 1/(x·ln10), d²(log10(x))/dx² = -1/(x²·ln10) graph->add_edge(result, x, inv * inv_ln10, -inv * inv * inv_ln10); return result; @@ -1505,7 +1603,7 @@ HYPERGRAPH_INLINE Variable erf(const Variable& x) const auto exp_neg_x2 = exp(-x.value() * x.value()); const auto first_deriv = two_over_sqrt_pi * exp_neg_x2; const auto second_deriv = -2.0 * x.value() * first_deriv; - const Variable result = graph->new_tmp_variable(erf_x); + const Variable result = graph->new_tmp_variable(erf_x, "erf"); graph->add_edge(result, x, first_deriv, second_deriv); return result; } @@ -1526,7 +1624,7 @@ HYPERGRAPH_INLINE Variable erfc(const Variable& x) const auto exp_neg_x2 = exp(-x.value() * x.value()); const auto first_deriv = -two_over_sqrt_pi * exp_neg_x2; const auto second_deriv = 2.0 * x.value() * two_over_sqrt_pi * exp_neg_x2; - const Variable result = graph->new_tmp_variable(erfc_x); + const Variable result = graph->new_tmp_variable(erfc_x, "erfc"); graph->add_edge(result, x, first_deriv, second_deriv); return result; } @@ -1545,7 +1643,7 @@ HYPERGRAPH_INLINE Variable sigmoid(const Variable& x) const auto first_deriv = sig * (1.0 - sig); // d²(sigmoid)/dx² = sigmoid · (1 - sigmoid) · (1 - 2·sigmoid) const auto second_deriv = first_deriv * (1.0 - 2.0 * sig); - const Variable result = graph->new_tmp_variable(sig); + const Variable result = graph->new_tmp_variable(sig, "sigmoid"); graph->add_edge(result, x, first_deriv, second_deriv); return result; } @@ -1566,7 +1664,7 @@ HYPERGRAPH_INLINE Variable softplus(const Variable& x) const auto sig = exp_x / (1.0 + exp_x); // d²(softplus)/dx² = sigmoid(x)·(1-sigmoid(x)) const auto second_deriv = sig * (1.0 - sig); - const Variable result = graph->new_tmp_variable(softplus_x); + const Variable result = graph->new_tmp_variable(softplus_x, "softplus"); graph->add_edge(result, x, sig, second_deriv); return result; } @@ -1577,12 +1675,12 @@ HYPERGRAPH_INLINE Variable min(const Variable& x, const Variable& y) // Subgradient convention: at x == y, derivative w.r.t. x is 1, w.r.t. y is 0 if (x.value() <= y.value()) { HyperGraph* graph = x.graph(); - const Variable result = graph->new_tmp_variable(x.value()); + const Variable result = graph->new_tmp_variable(x.value(), "min"); graph->add_edge(result, x, y, 1.0, 0.0, 0.0); return result; } else { HyperGraph* graph = y.graph(); - const Variable result = graph->new_tmp_variable(y.value()); + const Variable result = graph->new_tmp_variable(y.value(), "min"); graph->add_edge(result, x, y, 0.0, 1.0, 0.0); return result; } @@ -1594,12 +1692,12 @@ HYPERGRAPH_INLINE Variable max(const Variable& x, const Variable& y) // Subgradient convention: at x == y, derivative w.r.t. x is 1, w.r.t. y is 0 if (x.value() >= y.value()) { HyperGraph* graph = x.graph(); - const Variable result = graph->new_tmp_variable(x.value()); + const Variable result = graph->new_tmp_variable(x.value(), "max"); graph->add_edge(result, x, y, 1.0, 0.0, 0.0); return result; } else { HyperGraph* graph = y.graph(); - const Variable result = graph->new_tmp_variable(y.value()); + const Variable result = graph->new_tmp_variable(y.value(), "max"); graph->add_edge(result, x, y, 0.0, 1.0, 0.0); return result; } diff --git a/tests/TestHyperGraph.py b/tests/TestHyperGraph.py index c17e3c6..79024c1 100644 --- a/tests/TestHyperGraph.py +++ b/tests/TestHyperGraph.py @@ -1353,6 +1353,137 @@ def test_num_variables(self): graph.new_variables([2, 3, 4]) assert_equal(graph.num_variables, 4) + # to_dot — computation graph visualization + + def test_to_dot_returns_string(self): + """to_dot() returns a non-empty string""" + graph = hg.HyperGraph() + + a, b = graph.new_variables([3, 4]) + result = a * b + + dot = graph.to_dot() + self.assertIsInstance(dot, str) + self.assertGreater(len(dot), 0) + + def test_to_dot_contains_digraph(self): + """to_dot() output starts with a valid DOT digraph""" + graph = hg.HyperGraph() + + a, b = graph.new_variables([3, 4]) + result = a + b + + dot = graph.to_dot() + self.assertIn('digraph ComputationGraph', dot) + self.assertTrue(dot.strip().endswith('}')) + + def test_to_dot_contains_variable_labels(self): + """to_dot() output contains variable labels x0, x1""" + graph = hg.HyperGraph() + + a, b = graph.new_variables([3, 4]) + result = a * b + + dot = graph.to_dot() + self.assertIn('x0', dot) + self.assertIn('x1', dot) + + def test_to_dot_contains_operation_labels(self): + """to_dot() output contains operation labels like *, +, sin""" + graph = hg.HyperGraph() + + a, b = graph.new_variables([3, 4]) + result = a * b + np.sin(a) + + dot = graph.to_dot() + self.assertIn('*', dot) + self.assertIn('+', dot) + self.assertIn('sin', dot) + + def test_to_dot_contains_edges(self): + """to_dot() output contains edges (-> arrows)""" + graph = hg.HyperGraph() + + a, b = graph.new_variables([3, 4]) + result = a + b + + dot = graph.to_dot() + self.assertIn('->', dot) + + def test_to_dot_empty_graph(self): + """to_dot() works on a graph with no variables""" + graph = hg.HyperGraph() + + dot = graph.to_dot() + self.assertIn('digraph ComputationGraph', dot) + + def test_to_dot_single_variable(self): + """to_dot() works with only variables and no operations""" + graph = hg.HyperGraph() + + a = graph.new_variable(5) + + dot = graph.to_dot() + self.assertIn('x0', dot) + + def test_to_dot_complex_expression(self): + """to_dot() handles complex expression with multiple ops""" + graph = hg.HyperGraph() + + x, y = graph.new_variables([2, 3]) + result = x.exp() + y.log() * x + + dot = graph.to_dot() + self.assertIn('exp', dot) + self.assertIn('log', dot) + self.assertIn('*', dot) + self.assertIn('+', dot) + + def test_to_dot_write_file(self): + """to_dot(path) writes a file and returns the DOT string""" + import tempfile + import os + + graph = hg.HyperGraph() + + a, b = graph.new_variables([3, 4]) + result = a * b + + dot_string = graph.to_dot() + + # Write DOT source to a temp file (no graphviz required) + with tempfile.NamedTemporaryFile(mode='w', suffix='.dot', delete=False) as f: + f.write(dot_string) + tmp_path = f.name + + try: + self.assertTrue(os.path.exists(tmp_path)) + with open(tmp_path) as f: + content = f.read() + self.assertIn('digraph ComputationGraph', content) + finally: + os.unlink(tmp_path) + + def test_to_dot_unary_ops(self): + """to_dot() labels unary operations correctly""" + graph = hg.HyperGraph() + + x = graph.new_variable(0.5) + result = x.sqrt() + + dot = graph.to_dot() + self.assertIn('sqrt', dot) + + def test_to_dot_negation(self): + """to_dot() labels negation correctly""" + graph = hg.HyperGraph() + + x = graph.new_variable(5) + result = -x + + dot = graph.to_dot() + self.assertIn('neg', dot) + if __name__ == '__main__': unittest.main() From 763981ec4235a95632c7a7ce7e5e021b67f326f6 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 20 May 2026 17:03:34 +0200 Subject: [PATCH 3/3] feat: add scipy as a dependency in the project --- uv.lock | 67 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index b67480d..6d2d79e 100644 --- a/uv.lock +++ b/uv.lock @@ -17,6 +17,7 @@ version = "0.5.0" source = { editable = "." } dependencies = [ { name = "numpy" }, + { name = "scipy" }, ] [package.dev-dependencies] @@ -25,7 +26,10 @@ dev = [ ] [package.metadata] -requires-dist = [{ name = "numpy" }] +requires-dist = [ + { name = "numpy" }, + { name = "scipy" }, +] [package.metadata.requires-dev] dev = [{ name = "pytest" }] @@ -142,3 +146,64 @@ sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc wheels = [ { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, ] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +]