Add files from test project

This commit is contained in:
Andreas Tsouchlos 2025-02-26 00:21:05 +01:00
parent d10c955c61
commit 9880cf6655
19 changed files with 3876 additions and 3 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
build/
.cache/
__pycache__/

4
Dockerfile Normal file
View File

@ -0,0 +1,4 @@
FROM alpine:3.21.3
RUN apk update
RUN apk add cmake make g++ python3 eigen-dev spdlog-dev

View File

@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 an.tsouchlos
Copyright (c) 2025 Andreas Tsouchlos
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

View File

@ -1,3 +1,42 @@
# homotopy-continuation-channel-decoding
# Homotopy Continuation Channel Decoding
A project attempting to use homotopy continuation to perform channel decoding.
A project using homotopy continuation methods to perform channel decoding.
This repository contains implementations in multiple programming languages,
that are independent of each other. E.g., for each programming language there
are dedicated examples.
| Directory | Content |
| ---------- | ------------------ |
| `docs/` | Documentation |
| `scripts/` | Utility scripts |
| `cpp/` | C++ source code |
| `python/` | Python source code |
## Use Python Code
```bash
$ python python/examples/toy_homotopy.py -o temp.csv
$ python scripts/plot_solution_curve.py -i temp.csv
```
## Use C++ Code
#### On Host System
```bash
$ cmake -B build -S cpp
$ cmake --build build
$ ./build/toy_example_cpp/toy_example -o temp.csv
$ python scripts/plot_solution_curve.py -i temp.csv
```
#### In Docker Container
```bash
$ docker build . -t hccd
$ docker run --rm -it -u `id -u`:`id -g` -v $PWD:$PWD -w $PWD hccd cmake -B build -S cpp
$ docker run --rm -it -u `id -u`:`id -g` -v $PWD:$PWD -w $PWD hccd cmake --build build
$ docker run --rm -it -u `id -u`:`id -g` -v $PWD:$PWD -w $PWD hccd ./build/toy_example_cpp/toy_example -o temp.csv
$ python scripts/plot_solution_curve.py -i temp.csv
```

15
cpp/CMakeLists.txt Normal file
View File

@ -0,0 +1,15 @@
cmake_minimum_required(VERSION 3.22)
project(HomotopyContinuation)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
find_package(spdlog REQUIRED)
find_package(Eigen3 REQUIRED)
add_library(hccd INTERFACE)
target_include_directories(hccd INTERFACE include)
add_subdirectory(examples)

View File

@ -0,0 +1 @@
add_subdirectory(toy_homotopy)

View File

@ -0,0 +1,7 @@
set(CMAKE_BuilD_TYPE Release)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_executable(toy_homotopy toy_homotopy.cpp)
target_link_libraries(toy_homotopy PRIVATE hccd spdlog::spdlog Eigen3::Eigen)
target_include_directories(toy_homotopy PRIVATE hccd lib/argparse/include)

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,187 @@
// STL includes
#include <expected>
// Library includes
#include <hccd/PathTracker.hpp>
#include <hccd/util.hpp>
#include <spdlog/spdlog.h>
// Project includes
#include "argparse.hpp"
//
//
// Homotopy definition
//
//
///
/// @brief Helper type implementing necessary functions for PathTracker
/// @details Toy example homotopy:
/// G = [[x1],
/// [x2]]
///
/// F = [[x1 + x2 ],
/// [x2 + 0.5]]
///
/// H = (1-t)*G + t*F
///
/// @details Note that
/// y := [[x1],
/// [x2],
/// [t]]
///
struct ToyHomotopy {
///
/// @brief Evaluate H at y
///
static Eigen::VectorXd evaluate_H(Eigen::VectorXd y) {
Eigen::VectorXd result(2);
double x1 = y(0);
double x2 = y(1);
double t = y(2);
result(0) = x1 + t * x2;
result(1) = x2 + t * 0.5;
return result;
}
///
/// @brief Evaluate Jacobian of H at y
///
static Eigen::MatrixXd evaluate_DH(Eigen::VectorXd y) {
Eigen::MatrixXd result(2, 3);
double x1 = y(0);
double x2 = y(1);
double t = y(2);
result(0, 0) = 1;
result(0, 1) = t;
result(0, 2) = x2;
result(1, 0) = 0;
result(1, 1) = 1;
result(1, 2) = 0.5;
return result;
}
};
//
//
// Perform path tracking
//
//
void tracker_example(hccd::Settings settings, std::size_t num_iterations,
std::string output = "temp.csv") {
hccd::PathTracker<ToyHomotopy> tracker{settings};
std::ofstream out;
if (output != "") {
out.open(output);
out << "x1b," << "x2b," << "tb," << "x1p," << "x2p," << "tp," << "x1e,"
<< "x2e," << "te," << "x1n," << "x2n," << "tn," << std::endl;
}
Eigen::VectorXd y = Eigen::VectorXd::Zero(3);
for (int i = 0; i < num_iterations; ++i) {
spdlog::info("Iteration {}", i);
auto res = tracker.transparent_step(y);
if (!res) {
spdlog::error(
"Newton corrector failed to converge on iteration {} ", i);
std::terminate();
}
Eigen::VectorXd y_start, y_prime, y_hat_e;
std::tie(y_start, y_prime, y_hat_e, y) = res.value();
spdlog::info("y:{}", y);
if (output != "") {
out << y_start(0) << "," << y_start(1) << "," << y_start(2) << ","
<< y_prime(0) << "," << y_prime(1) << "," << y_prime(2) << ","
<< y_hat_e(0) << "," << y_hat_e(1) << "," << y_hat_e(2) << ","
<< y(0) << "," << y(1) << "," << y(2) << std::endl;
}
}
}
//
//
// User Interface
//
//
int main(int argc, char* argv[]) {
// Parse command line arguments
argparse::ArgumentParser program("Homotopy continuation path tracker");
program.add_argument("--verbose")
.default_value<bool>(false)
.implicit_value(true);
program.add_argument("--euler-step-size")
.help("Step size for Euler predictor")
.default_value<double>(0.05)
.scan<'g', double>();
program.add_argument("--euler-max-tries")
.help("Maximum number of tries for Euler predictor")
.default_value<unsigned>(5)
.scan<'u', unsigned>();
program.add_argument("--newton-max-iter")
.help("Maximum number of iterations for Newton corrector")
.default_value<unsigned>(5)
.scan<'u', unsigned>();
program.add_argument("--newton-convergence-threshold")
.help("Convergence threshold for Newton corrector")
.default_value<double>(0.01)
.scan<'g', double>();
program.add_argument("-s", "--sigma")
.help("Direction in which the path is traced")
.default_value<int>(1)
.scan<'i', int>();
program.add_argument("-o", "--output")
.help("Output csv file")
.default_value<std::string>("");
program.add_argument("-n", "--num-iterations")
.help("Number of iterations of the example program to run")
.default_value<std::size_t>(20)
.scan<'u', std::size_t>();
try {
program.parse_args(argc, argv);
} catch (const std::runtime_error& err) {
spdlog::error("{}", err.what());
std::terminate();
}
// Run program
if (program["--verbose"] == true) spdlog::set_level(spdlog::level::debug);
hccd::Settings settings{
.euler_step_size = program.get<double>("--euler-step-size"),
.euler_max_tries = program.get<unsigned>("--euler-max-tries"),
.newton_max_iter = program.get<unsigned>("--newton-max-iter"),
.newton_convergence_threshold =
program.get<double>("--newton-convergence-threshold"),
.sigma = program.get<int>("--sigma"),
};
tracker_example(settings, program.get<std::size_t>("--num-iterations"),
program.get<std::string>("--output"));
}

View File

@ -0,0 +1,156 @@
#pragma once
// STL includes
#include <expected>
// Library includes
#include <Eigen/Dense>
#include <spdlog/spdlog.h>
// Project includes
#include "util.hpp"
namespace hccd {
namespace detail {
template <typename T>
concept homotopy_c = requires(T) {
{ T::evaluate_H(Eigen::VectorXd()) } -> std::same_as<Eigen::VectorXd>;
{ T::evaluate_DH(Eigen::VectorXd()) } -> std::same_as<Eigen::MatrixXd>;
};
} // namespace detail
///
/// @brief Settings for PathTracker
///
struct Settings {
double euler_step_size = 0.05;
unsigned euler_max_tries = 5;
unsigned newton_max_iter = 5;
double newton_convergence_threshold = 0.01;
int sigma = 1; ///< Direction in which the path is traced
};
///
/// @brief Path tracker for the homotopy continuation method
/// @details Uses a predictor-corrector scheme to trace a path defined by a
/// homotopy.
/// @details References:
/// [1] T. Chen and T.-Y. Li, “Homotopy continuation method for solving
/// systems of nonlinear and polynomial equations,” Communications in
/// Information and Systems, vol. 15, no. 2, pp. 119307, 2015
///
/// @tparam homotopy_c Homotopy defining the path
///
template <detail::homotopy_c Homotopy>
class PathTracker {
public:
enum Error { NewtonNotConverged };
PathTracker(Settings settings) : m_settings{settings} {
}
///
/// @brief Perform one predictor-corrector step
///
Eigen::VectorXd step(Eigen::VectorXd y) {
auto res = transparent_step(y);
if (res) {
return res.value().first;
} else {
return std::unexpected(res.error());
}
}
///
/// @brief Perform one predictor-corrector step, returning intermediate
/// results
///
std::expected<std::tuple<Eigen::VectorXd, Eigen::VectorXd, Eigen::VectorXd,
Eigen::VectorXd>,
Error>
transparent_step(Eigen::VectorXd y) {
for (int i = 0; i < m_settings.euler_max_tries; ++i) {
double step_size = m_settings.euler_step_size / (1 << i);
const auto [y_hat, y_prime] =
perform_euler_predictor_step(y, step_size);
auto res = perform_newton_corrector_step(y_hat);
if (res) return {{y, y_prime, y_hat, res.value()}};
}
return std::unexpected(Error::NewtonNotConverged);
}
private:
Settings m_settings;
std::pair<Eigen::VectorXd, Eigen::VectorXd>
perform_euler_predictor_step(Eigen::VectorXd y, double step_size) {
/// Obtain y_prime
Eigen::MatrixXd DH = Homotopy::evaluate_DH(y);
auto qr = DH.transpose().colPivHouseholderQr();
Eigen::MatrixXd Q = qr.matrixQ();
Eigen::MatrixXd R = qr.matrixR();
Eigen::VectorXd y_prime = Q.col(2);
spdlog::debug("Q: \t\t{}x{}; det={}", Q.rows(), Q.cols(),
Q.determinant());
spdlog::debug("R.topRows(2): {}x{}; det={}", R.topRows(2).rows(),
R.topRows(2).cols(), R.topRows(2).determinant());
if (sign(Q.determinant() * R.topRows(2).determinant()) !=
sign(m_settings.sigma))
y_prime = -y_prime;
/// Perform prediction
Eigen::VectorXd y_hat = y + step_size * y_prime;
return {y_hat, y_prime};
}
std::expected<Eigen::VectorXd, Error>
perform_newton_corrector_step(Eigen::VectorXd y) {
Eigen::VectorXd prev_y = y;
for (int i = 0; i < m_settings.newton_max_iter; ++i) {
/// Perform correction
Eigen::MatrixXd DH = Homotopy::evaluate_DH(y);
Eigen::MatrixXd DH_pinv =
DH.completeOrthogonalDecomposition().pseudoInverse();
y = y - DH_pinv * Homotopy::evaluate_H(y);
/// Check stopping criterion
spdlog::debug("Newton iteration {}: ||y-prev_y||={}", i,
(y - prev_y).norm());
if ((y - prev_y).norm() < m_settings.newton_convergence_threshold)
return y;
prev_y = y;
}
return std::unexpected(Error::NewtonNotConverged);
}
template <typename T>
static int sign(T val) {
return -1 * (val < T(0)) + 1 * (val >= T(0));
}
};
} // namespace hccd

37
cpp/include/hccd/util.hpp Normal file
View File

@ -0,0 +1,37 @@
#pragma once
#include <Eigen/Dense>
// #include <Eigen/src/Core/util/ForwardDeclarations.h>
#include <spdlog/fmt/ostr.h>
#include <spdlog/spdlog.h>
/// @brief Boilerplate code to enable fmtlib to print Eigen::MatrixXd
template <>
struct fmt::formatter<Eigen::MatrixXd> : fmt::ostream_formatter {};
/// @brief Boilerplate code to enable fmtlib to print Eigen::VectorXd
template <>
struct fmt::formatter<Eigen::VectorXd> : fmt::ostream_formatter {
template <typename Context>
auto format(const Eigen::VectorXd& value, Context& ctx) const
-> decltype(ctx.out()) {
auto buffer = basic_memory_buffer<char>();
auto&& formatbuf =
detail::formatbuf<std::basic_streambuf<char>>(buffer);
auto&& output = std::basic_ostream<char>(&formatbuf);
output.imbue(std::locale::classic());
output << "[";
for (int i = 0; i < value.size(); ++i) {
output << value(i);
if (i < value.size() - 1) output << ", ";
}
output << "]";
output.exceptions(std::ios_base::failbit | std::ios_base::badbit);
return formatter<basic_string_view<char>, char>::format(
{buffer.data(), buffer.size()}, ctx);
}
};

99
docs/general_idea.md Normal file
View File

@ -0,0 +1,99 @@
# Homotopy Continuation
### Introduction
The aim of a homotopy method consists in solving a system of N nonlinear
equations in N variables \[1, p.1\]:
$$
F(\bm{x}) = 0, \hspace{5mm} F: \mathbb{R}^N \rightarrow \mathbb{R}^N
$$
This is achieved by defining a homotopy, or deformation,
$H : \mathbb{R}^N \rightarrow \mathbb{R}^N$ such that
$$
H(\bm{x}, 0) = G(\bm{x}), \hspace{5mm} H(\bm{x},1) = F(\bm{x}),
$$
where $G: \mathbb{R}^N \rightarrow \mathbb{R}^N$ is trivial, having known zero
points. One can then trace an implicitly defined curve
$\bm{c}(s) \in H^{-1}(0)$ from $(\bm{x}_1,0)$ to $(\bm{x}_2, 1)$ [2, p.122].
Typically, a convex homotopy such as
$$
H(\bm{x}, t) := tG(\bm{x}) + (1-t)F(\bm{x})
$$
is chosen [1, p.2].
### Implicit Definition of Solution Curve
"In the construction of the homotopy H(x, t) = 0, it may seem natural to use t
as the designated parameter for the solution curve [...]. However, this
parametrization has a severe limitation, as t cannot be used as a smooth
parameter in certain situations. For example, [...] at points where ∂H/∂x is
singular the solution curve of H(x, t) = 0 cannot be parametrized by t
directly. Actually, one may avoid such difficulties by considering both x and t
as independent variables and parametrize the smooth curve γ by the arc-length."
[2, p. 125-126].
We can show that the solution curve can be implicitly defined as
$$
\begin{align}
DH(\bm{y}(s))\cdot \dot{\bm{y}}(s) = 0 \\[.5em]
\text{det}\left(\begin{array}{c} DH(\bm{y}(s)) \\ \dot{\bm{y}}(s)\end{array}\right) = \sigma_0 \\[.5em]
\lVert \dot{\bm{y}}(s) \rVert = 1 \\[.5em]
\bm{y}(0) = (\bm{x}_0, 0)
.
\end{align}
$$
where $DH(y)$ is the Jacobian of $H(y)$ [2, p.127] [1, p.9]. Equation (1)
corresponds to $\dot{y}(s)$ being tangent to the solution curve, the purpose of
(2) is to fix the direction in which the curve is traced.
### Tracing solution curves
"In principle, any of the available ODE solvers capable of integrating the
above system can be used to trace the curve [...]. However, due to numerical
stability concerns, a more preferable method to trace the curve is the
'prediction-correction scheme'. [...] Among many different choices for the
'predictors' as well as 'correctors', we shall present here a commonly used
combination of the (generalized) Eulers method for the predictor and Newtons
method for the corrector." [2, p.127]
#### Euler's Predictor
$$
\hat{\bm{y}} = \bm{y}_0 + \Delta s \cdot \sigma \cdot \Delta \bm{y},
$$
with $\Delta s$ denoting the step size and $\Delta y$ the numerical
approximation of $\dot{y}(s)$ [2, p.128].
#### Newton's corrector
$$
\bm{y} = \mathcal{N}^k(\hat{\bm{y}}), \hspace{5mm} \mathcal{N}(\hat{\bm{y}}) := \hat{\bm{y}} - (DH(\hat{\bm{y}}))^{+} H(\hat{\bm{y}}).
$$
" [...] the shrinking distance
$\lVert \mathcal{N}^j(\hat{\bm{y}}) - \mathcal{N}^{j-1} (\hat{\bm{y}})\rVert$
between successive points produced by the iterations can be used as a criterion
for convergence. Of course, if the iterations fail to converge, one must go
back to adjust the step size for the Eulers predictor." [2, p.130]
______________________________________________________________________
## References
\[1\]: E. L. Allgower and K. Georg, Introduction to numerical continuation
methods. in Classics in applied mathematics, no. 45. Philadelphia, Pa: Society
for Industrial and Applied Mathematics (SIAM, 3600 Market Street, Floor 6,
Philadelphia, PA 19104), 2003. doi: 10.1137/1.9780898719154.
\[2\]: T. Chen and T.-Y. Li, “Homotopy continuation method for solving systems
of nonlinear and polynomial equations,” Communications in Information and
Systems, vol. 15, no. 2, pp. 119307, 2015, doi: 10.4310/CIS.2015.v15.n2.a1.

View File

@ -0,0 +1,142 @@
import argparse
import numpy as np
import pandas as pd
# autopep8: off
import sys
import os
sys.path.append(f"{os.path.dirname(os.path.abspath(__file__))}/../")
# TODO: How do I import PathTracker and HomotopyGenerator properly?
from hccd import path_tracker, homotopy_generator
# autopep8: on
# class RepetitionCodeHomotopy:
# """Helper type implementing necessary functions for PathTracker.
#
# Repetiton code homotopy:
# G = [[x1],
# [x2],
# [x1]]
#
# F = [[1 - x1**2],
# [1 - x2**2],
# [1 - x1*x2]]
#
# H = (1-t)*G + t*F
#
# Note that
# y := [[x1],
# [x2],
# [t]]
# """
# @staticmethod
# def evaluate_H(y: np.ndarray) -> np.ndarray:
# """Evaluate H at y."""
# x1 = y[0]
# x2 = y[1]
# t = y[2]
#
# print(y)
#
# result = np.zeros(shape=3)
# result[0] = -t*x1**2 + x1*(1-t) + t
# result[1] = -t*x2**2 + x2*(1-t) + t
# result[2] = -t*x1*x2 + x1*(1-t) + t
#
# return result
#
# @staticmethod
# def evaluate_DH(y: np.ndarray) -> np.ndarray:
# """Evaluate Jacobian of H at y."""
# x1 = y[0]
# x2 = y[1]
# t = y[2]
#
# result = np.zeros(shape=(3, 3))
# result[0, 0] = -2*t*x1 + (1-t)
# result[0, 1] = 0
# result[0, 2] = -x1**2 - x1 + 1
# result[1, 0] = 0
# result[1, 1] = -2*t*x2 + (1-t)
# result[1, 2] = -x2**2 - x2 + 1
# result[1, 0] = -t*x2 + (1-t)
# result[1, 1] = -t*x1
# result[1, 2] = -x1*x2 - x1 + 1
#
# return result
def track_path(args):
H = np.array([[1, 1, 1]])
homotopy = homotopy_generator.HomotopyGenerator(H)
tracker = path_tracker.PathTracker(homotopy, args.euler_step_size, args.euler_max_tries,
args.newton_max_iter, args.newton_convergence_threshold, args.sigma)
ys_start, ys_prime, ys_hat_e, ys = [], [], [], []
try:
y = np.zeros(3)
for i in range(args.num_iterations):
y_start, y_prime, y_hat_e, y = tracker.transparent_step(y)
ys_start.append(y_start)
ys_prime.append(y_prime)
ys_hat_e.append(y_hat_e)
ys.append(y)
print(f"Iteration {i}: {y}")
except Exception as e:
print(f"Error: {e}")
ys_start = np.array(ys_start)
ys_prime = np.array(ys_prime)
ys_hat_e = np.array(ys_hat_e)
ys = np.array(ys)
df = pd.DataFrame({"x1b": ys_start[:, 0],
"x2b": ys_start[:, 1],
"tb": ys_start[:, 2],
"x1p": ys_prime[:, 0],
"x2p": ys_prime[:, 1],
"tp": ys_prime[:, 2],
"x1e": ys_hat_e[:, 0],
"x2e": ys_hat_e[:, 1],
"te": ys_hat_e[:, 2],
"x1n": ys[:, 0],
"x2n": ys[:, 1],
"tn": ys[:, 2]
})
if args.output:
df.to_csv(args.output, index=False)
else:
print(df)
def main():
parser = argparse.ArgumentParser(
description='Homotopy continuation path tracker')
parser.add_argument("--verbose", default=False, action='store_true')
parser.add_argument("--euler-step-size", type=float,
default=0.05, help="Step size for Euler predictor")
parser.add_argument("--euler-max-tries", type=int, default=5,
help="Maximum number of tries for Euler predictor")
parser.add_argument("--newton-max-iter", type=int, default=5,
help="Maximum number of iterations for Newton corrector")
parser.add_argument("--newton-convergence-threshold", type=float,
default=0.01, help="Convergence threshold for Newton corrector")
parser.add_argument("-s", "--sigma", type=int, default=1,
help="Direction in which the path is traced")
parser.add_argument("-o", "--output", type=str, help="Output csv file")
parser.add_argument("-n", "--num-iterations", type=int, default=20,
help="Number of iterations of the example program to run")
args = parser.parse_args()
track_path(args)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,131 @@
import argparse
import numpy as np
import pandas as pd
# autopep8: off
import sys
import os
sys.path.append(f"{os.path.dirname(os.path.abspath(__file__))}/../")
# TODO: How do I import PathTracker and HomotopyGenerator properly?
from hccd import path_tracker, homotopy_generator
# autopep8: on
class ToyHomotopy:
"""Helper type implementing necessary functions for PathTracker.
Toy example homotopy:
G = [[x1],
[x2]]
F = [[x1 + x2 ],
[x2 + 0.5]]
H = (1-t)*G + t*F
Note that
y := [[x1],
[x2],
[t]]
"""
@staticmethod
def evaluate_H(y: np.ndarray) -> np.ndarray:
"""Evaluate H at y."""
x1 = y[0]
x2 = y[1]
t = y[2]
result = np.zeros(shape=2)
result[0] = x1 + t * x2
result[1] = x2 + t * 0.5
return result
@staticmethod
def evaluate_DH(y: np.ndarray) -> np.ndarray:
"""Evaluate Jacobian of H at y."""
x1 = y[0]
x2 = y[1]
t = y[2]
result = np.zeros(shape=(2, 3))
result[0, 0] = 1
result[0, 1] = t
result[0, 2] = x2
result[1, 0] = 0
result[1, 1] = 1
result[1, 2] = 0.5
return result
def track_path(args):
tracker = path_tracker.PathTracker(ToyHomotopy, args.euler_step_size, args.euler_max_tries,
args.newton_max_iter, args.newton_convergence_threshold, args.sigma)
ys_start, ys_prime, ys_hat_e, ys = [], [], [], []
try:
y = np.zeros(3)
for i in range(args.num_iterations):
y_start, y_prime, y_hat_e, y = tracker.transparent_step(y)
ys_start.append(y_start)
ys_prime.append(y_prime)
ys_hat_e.append(y_hat_e)
ys.append(y)
print(f"Iteration {i}: {y}")
except Exception as e:
print(f"Error: {e}")
ys_start = np.array(ys_start)
ys_prime = np.array(ys_prime)
ys_hat_e = np.array(ys_hat_e)
ys = np.array(ys)
df = pd.DataFrame({"x1b": ys_start[:, 0],
"x2b": ys_start[:, 1],
"tb": ys_start[:, 2],
"x1p": ys_prime[:, 0],
"x2p": ys_prime[:, 1],
"tp": ys_prime[:, 2],
"x1e": ys_hat_e[:, 0],
"x2e": ys_hat_e[:, 1],
"te": ys_hat_e[:, 2],
"x1n": ys[:, 0],
"x2n": ys[:, 1],
"tn": ys[:, 2]
})
if args.output:
df.to_csv(args.output, index=False)
else:
print(df)
def main():
parser = argparse.ArgumentParser(
description='Homotopy continuation path tracker')
parser.add_argument("--verbose", default=False, action='store_true')
parser.add_argument("--euler-step-size", type=float,
default=0.05, help="Step size for Euler predictor")
parser.add_argument("--euler-max-tries", type=int, default=5,
help="Maximum number of tries for Euler predictor")
parser.add_argument("--newton-max-iter", type=int, default=5,
help="Maximum number of iterations for Newton corrector")
parser.add_argument("--newton-convergence-threshold", type=float,
default=0.01, help="Convergence threshold for Newton corrector")
parser.add_argument("-s", "--sigma", type=int, default=1,
help="Direction in which the path is traced")
parser.add_argument("-o", "--output", type=str, help="Output csv file")
parser.add_argument("-n", "--num-iterations", type=int, default=20,
help="Number of iterations of the example program to run")
args = parser.parse_args()
track_path(args)
if __name__ == '__main__':
main()

0
python/hccd/__init__.py Normal file
View File

6
python/hccd/__main__.py Normal file
View File

@ -0,0 +1,6 @@
def main():
pass
if __name__ == "__main__":
main()

View File

@ -0,0 +1,117 @@
import numpy as np
import sympy as sp
from typing import List, Callable
class HomotopyGenerator:
"""Generates homotopy functions from a binary parity check matrix."""
def __init__(self, parity_check_matrix: np.ndarray):
"""
Initialize with a parity check matrix.
Args:
parity_check_matrix: Binary matrix where rows represent parity checks
and columns represent variables.
"""
self.H_matrix = parity_check_matrix
self.num_checks, self.num_vars = parity_check_matrix.shape
# Create symbolic variables
self.x_vars = [sp.symbols(f'x{i+1}') for i in range(self.num_vars)]
self.t = sp.symbols('t')
# Generate G, F, and H
self.G = self._create_G()
self.F = self._create_F()
self.H = self._create_H()
# Convert to callable functions
self._H_lambda = self._create_H_lambda()
self._DH_lambda = self._create_DH_lambda()
def _create_G(self) -> List[sp.Expr]:
"""Create G polynomial system (the starting system)."""
# For each variable xi, add the polynomial [xi]
G = []
for var in self.x_vars:
G.append(var)
return G
def _create_F(self) -> List[sp.Expr]:
"""Create F polynomial system (the target system)."""
F = []
# Add 1 - xi^2 for each variable
for var in self.x_vars:
F.append(1 - var**2)
# Add parity check polynomials: 1 - x1*x2*...*xk for each parity check
for row in self.H_matrix:
# Create product of variables that participate in this check
term = 1
for i, bit in enumerate(row):
if bit == 1:
term *= self.x_vars[i]
if term != 1: # Only add if there are variables in this check
F.append(1 - term)
return F
def _create_H(self) -> List[sp.Expr]:
"""Create the homotopy H = (1-t)*G + t*F."""
H = []
# Make sure G and F have the same length
# Repeat variables from G if needed to match F's length
G_extended = self.G.copy()
while len(G_extended) < len(self.F):
# Cycle through variables to repeat
for i in range(min(self.num_vars, len(self.F) - len(G_extended))):
G_extended.append(self.x_vars[i % self.num_vars])
# Create the homotopy
for g, f in zip(G_extended, self.F):
H.append((1 - self.t) * g + self.t * f)
return H
def _create_H_lambda(self) -> Callable:
"""Create a lambda function to evaluate H."""
all_vars = self.x_vars + [self.t]
return sp.lambdify(all_vars, self.H, 'numpy')
def _create_DH_lambda(self) -> Callable:
"""Create a lambda function to evaluate the Jacobian of H."""
all_vars = self.x_vars + [self.t]
jacobian = sp.Matrix([[sp.diff(expr, var)
for var in all_vars] for expr in self.H])
return sp.lambdify(all_vars, jacobian, 'numpy')
def evaluate_H(self, y: np.ndarray) -> np.ndarray:
"""
Evaluate H at point y.
Args:
y: Array of form [x1, x2, ..., xn, t] where xi are the variables
and t is the homotopy parameter.
Returns:
Array containing H evaluated at y.
"""
return np.array(self._H_lambda(*y))
def evaluate_DH(self, y: np.ndarray) -> np.ndarray:
"""
Evaluate the Jacobian of H at point y.
Args:
y: Array of form [x1, x2, ..., xn, t] where xi are the variables
and t is the homotopy parameter.
Returns:
Matrix containing the Jacobian of H evaluated at y.
"""
return np.array(self._DH_lambda(*y), dtype=float)

View File

@ -0,0 +1,84 @@
import numpy as np
import typing
import scipy
def _sign(val):
return -1 * (val < 0) + 1 * (val >= 0)
class PathTracker:
"""
Path trakcer for the homotopy continuation method. Uses a
predictor-corrector scheme to trace a path defined by a homotopy.
References:
[1] T. Chen and T.-Y. Li, Homotopy continuation method for solving
systems of nonlinear and polynomial equations, Communications in
Information and Systems, vol. 15, no. 2, pp. 119307, 2015
"""
def __init__(self, Homotopy, euler_step_size=0.05, euler_max_tries=10, newton_max_iter=5,
newton_convergence_threshold=0.001, sigma=1):
self.Homotopy = Homotopy
self._euler_step_size = euler_step_size
self._euler_max_tries = euler_max_tries
self._newton_max_iter = newton_max_iter
self._newton_convergence_threshold = newton_convergence_threshold
self._sigma = sigma
def step(self, y):
"""Perform one predictor-corrector step."""
return self.transparent_step(y)[0]
def transparent_step(self, y) -> typing.Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray,]:
"""Perform one predictor-corrector step, returning intermediate results."""
for i in range(self._euler_max_tries):
step_size = self._euler_step_size / (1 << i)
y_hat, y_prime = self._perform_euler_predictor_step(y, step_size)
y_hat_n = self._perform_newtown_corrector_step(y_hat)
return y, y_prime, y_hat, y_hat_n
raise RuntimeError("Newton corrector did not converge")
def _perform_euler_predictor_step(self, y, step_size) -> typing.Tuple[np.ndarray, np.ndarray]:
# Obtain y_prime
DH = self.Homotopy.evaluate_DH(y)
ns = scipy.linalg.null_space(DH)
y_prime = ns[:, 0] * self._sigma
# Q, R = np.linalg.qr(np.transpose(DH), mode="complete")
# y_prime = Q[:, 2]
#
# if _sign(np.linalg.det(Q)*np.linalg.det(R[:2, :])) != _sign(self._sigma):
# y_prime = -y_prime
# Perform prediction
y_hat = y + step_size*y_prime
return y_hat, y_prime
def _perform_newtown_corrector_step(self, y) -> np.ndarray:
prev_y = y
for _ in range(self._newton_max_iter):
# Perform correction
DH = self.Homotopy.evaluate_DH(y)
DH_pinv = np.linalg.pinv(DH)
y = y - DH_pinv @ self.Homotopy.evaluate_H(y)
# Check stopping criterion
if np.linalg.norm(y - prev_y) < self._newton_convergence_threshold:
return y
prev_y = y
raise RuntimeError("Newton corrector did not converge")

View File

@ -0,0 +1,106 @@
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import argparse
def plot_2d(df: pd.DataFrame):
df = pd.read_csv("temp.csv")
fig = plt.figure()
plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.95)
ax = fig.add_subplot()
# Analytically computed solution
ts = df['tb']
x2s = -ts / 2
x1s = -ts * x2s
ax.scatter(x1s, x2s)
ax.set_xlabel("x1")
ax.set_ylabel("x2")
# ax.set_zlabel("t")
df = df.reset_index()
for _, row in df.iterrows():
x1b = row['x1b']
x2b = row['x2b']
# x1p = row['x1p']
# x2p = row['x2p']
x1e = row['x1e']
x2e = row['x2e']
x1n = row['x1n']
x2n = row['x2n']
ax.plot([x1b, x1e], [x2b, x2e], 'b')
ax.plot([x1e, x1n], [x2e, x2n], 'r')
plt.show()
def plot_3d(df: pd.DataFrame):
df = pd.read_csv("temp.csv")
fig = plt.figure()
plt.subplots_adjust(left=0.05, right=0.95, bottom=0.05, top=0.95)
ax = fig.add_subplot(projection='3d')
# Analytically computed solution
ts = df['tb']
x2s = -ts / 2
x1s = -ts * x2s
ax.scatter(x1s, x2s)
ax.scatter(x1s, x2s, ts)
ax.set_xlabel("x1")
ax.set_ylabel("x2")
ax.set_zlabel("t")
df = df.reset_index()
for _, row in df.iterrows():
x1b = row['x1b']
x2b = row['x2b']
tb = row['tb']
# x1p = row['x1p']
# x2p = row['x2p']
# tp = row['tp']
x1e = row['x1e']
x2e = row['x2e']
te = row['te']
x1n = row['x1n']
x2n = row['x2n']
tn = row['tn']
ax.plot([x1b, x1e], [x2b, x2e], 'b', zs=[tb, te])
ax.plot([x1e, x1n], [x2e, x2n], 'r', zs=[te, tn])
plt.show()
def main():
# Parse command line arguments
parser = argparse.ArgumentParser()
parser.add_argument("--input", "-i", type=str, required=True,
default="temp.csv", help="Filename of the csv file")
parser.add_argument("--projection", "-p", type=str, required=False,
default="2d", help="2d or 3d plot")
args = parser.parse_args()
# Plot data
df = pd.read_csv(args.input)
if args.projection == "2d":
plot_2d(df)
elif args.projection == "3d":
plot_3d(df)
if __name__ == "__main__":
main()