Skip to content

Commit 907118e

Browse files
committed
added linear solver
1 parent 64cd8a1 commit 907118e

4 files changed

Lines changed: 299 additions & 7 deletions

File tree

README.md

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# polysolve-python
22

3-
Small Python binding for PolySolve's nonlinear solver interface.
3+
Small Python binding for PolySolve's nonlinear and linear solver interfaces.
44

55
```python
66
import numpy as np
@@ -20,7 +20,7 @@ class Quadratic(polysolve.Problem):
2020
return 2.0 * scipy.sparse.eye(x.size, format="csc")
2121

2222

23-
x, result = polysolve.minimize(
23+
x, log = polysolve.minimize(
2424
Quadratic(),
2525
np.zeros(3),
2626
{
@@ -32,7 +32,29 @@ x, result = polysolve.minimize(
3232
)
3333

3434
print(x)
35-
print(result)
35+
print(log)
3636
```
3737

3838
Python subclasses must implement `value(x)`, `gradient(x)`, and `hessian(x)`. Optional PolySolve callbacks such as `solution_changed`, `stop`, `is_step_valid`, and `max_step_size` can also be implemented on the subclass.
39+
40+
## Linear solves
41+
42+
For a one-off linear system:
43+
44+
```python
45+
A = scipy.sparse.csc_matrix([[4.0, 1.0], [1.0, 3.0]])
46+
b = np.array([1.0, 2.0])
47+
48+
x = polysolve.solve(A, b, {"solver": "Eigen::SimplicialLDLT"})
49+
```
50+
51+
For repeated solves with the same matrix pattern:
52+
53+
```python
54+
solver = polysolve.LinearSolver({"solver": "Eigen::SimplicialLDLT"})
55+
solver.analyze_pattern(A)
56+
solver.factorise(A) # factorize(A) is also available
57+
58+
x = solver.solve(b)
59+
print(solver.info())
60+
```

cmake/polysolve.cmake

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ endif()
88
message(STATUS "Third-party: creating target 'polysolve'")
99

1010
include(CPM)
11-
CPMAddPackage("gh:polyfem/polysolve#550f6a51fd1cf2904ac76b748c6bd0d3c20aed89")
11+
CPMAddPackage("gh:polyfem/polysolve#59cd6d6271d73a69236e7c714bfc580527b33fac")

example.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def hessian(self, x):
1717
def post_step(self, iter_num, solver_info, x, grad):
1818
print(f"Iteration {iter_num}: x = {x}, grad = {grad}")
1919

20-
x, result = polysolve.minimize(
20+
x, log = polysolve.minimize(
2121
Quadratic(),
2222
np.zeros(3),
2323
{
@@ -29,4 +29,4 @@ def post_step(self, iter_num, solver_info, x, grad):
2929
)
3030

3131
print("Optimal point:", x)
32-
print("Optimal value:", result)
32+
print("Optimal value:", log)

src/polysolve.cpp

Lines changed: 271 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <pybind11_json/pybind11_json.hpp>
99

1010
#include <polysolve/Types.hpp>
11+
#include <polysolve/linear/Solver.hpp>
1112
#include <polysolve/nonlinear/Problem.hpp>
1213
#include <polysolve/nonlinear/Solver.hpp>
1314

@@ -27,6 +28,7 @@ namespace
2728
using TVector = Problem::TVector;
2829
using TMatrix = Problem::TMatrix;
2930
using THessian = Problem::THessian;
31+
using StiffnessMatrix = polysolve::StiffnessMatrix;
3032

3133
std::shared_ptr<spdlog::logger> python_logger(const spdlog::level::level_enum log_level)
3234
{
@@ -101,6 +103,55 @@ namespace
101103
}
102104
}
103105

106+
StiffnessMatrix cast_sparse_matrix(const py::object &obj)
107+
{
108+
StiffnessMatrix A;
109+
try
110+
{
111+
A = obj.cast<StiffnessMatrix>();
112+
}
113+
catch (const py::cast_error &)
114+
{
115+
A = obj.cast<TMatrix>().sparseView();
116+
}
117+
118+
if (A.rows() != A.cols())
119+
{
120+
throw std::runtime_error(
121+
"linear system matrix must be square, got shape (" + std::to_string(A.rows()) + ", "
122+
+ std::to_string(A.cols()) + ")");
123+
}
124+
125+
return A;
126+
}
127+
128+
TMatrix cast_dense_matrix(const py::object &obj)
129+
{
130+
TMatrix A;
131+
try
132+
{
133+
A = obj.cast<TMatrix>();
134+
}
135+
catch (const py::cast_error &)
136+
{
137+
A = TMatrix(obj.cast<StiffnessMatrix>());
138+
}
139+
140+
if (A.rows() != A.cols())
141+
{
142+
throw std::runtime_error(
143+
"linear system matrix must be square, got shape (" + std::to_string(A.rows()) + ", "
144+
+ std::to_string(A.cols()) + ")");
145+
}
146+
147+
return A;
148+
}
149+
150+
int default_precond_num(const Eigen::Index rows, const int precond_num)
151+
{
152+
return precond_num < 0 ? static_cast<int>(rows) : precond_num;
153+
}
154+
104155
class PyProblem : public Problem
105156
{
106157
public:
@@ -205,15 +256,131 @@ namespace
205256
}
206257
};
207258

259+
class LinearSolver
260+
{
261+
public:
262+
LinearSolver(
263+
const py::dict &solver_params,
264+
const spdlog::level::level_enum log_level,
265+
const bool strict_validation)
266+
: logger_(python_logger(log_level))
267+
{
268+
py::scoped_ostream_redirect stdout_redirect(
269+
std::cout, py::module_::import("sys").attr("stdout"));
270+
const polysolve::json params = solver_params.cast<polysolve::json>();
271+
solver_ = polysolve::linear::Solver::create(params, *logger_, strict_validation);
272+
}
273+
274+
void set_parameters(const py::dict &params)
275+
{
276+
const polysolve::json jparams = params.cast<polysolve::json>();
277+
solver_->set_parameters(jparams);
278+
}
279+
280+
void analyze_pattern(const py::object &A_obj, const int precond_num)
281+
{
282+
py::scoped_ostream_redirect stdout_redirect(
283+
std::cout, py::module_::import("sys").attr("stdout"));
284+
285+
if (solver_->is_dense())
286+
{
287+
TMatrix A = cast_dense_matrix(A_obj);
288+
const int actual_precond_num = default_precond_num(A.rows(), precond_num);
289+
py::gil_scoped_release release;
290+
solver_->analyze_pattern_dense(A, actual_precond_num);
291+
}
292+
else
293+
{
294+
StiffnessMatrix A = cast_sparse_matrix(A_obj);
295+
const int actual_precond_num = default_precond_num(A.rows(), precond_num);
296+
py::gil_scoped_release release;
297+
solver_->analyze_pattern(A, actual_precond_num);
298+
}
299+
}
300+
301+
void factorize(const py::object &A_obj)
302+
{
303+
py::scoped_ostream_redirect stdout_redirect(
304+
std::cout, py::module_::import("sys").attr("stdout"));
305+
306+
if (solver_->is_dense())
307+
{
308+
TMatrix A = cast_dense_matrix(A_obj);
309+
py::gil_scoped_release release;
310+
solver_->factorize_dense(A);
311+
factorized_size_ = A.rows();
312+
}
313+
else
314+
{
315+
StiffnessMatrix A = cast_sparse_matrix(A_obj);
316+
py::gil_scoped_release release;
317+
solver_->factorize(A);
318+
factorized_size_ = A.rows();
319+
}
320+
}
321+
322+
TVector solve(const TVector &b, const py::object &x0_obj)
323+
{
324+
if (factorized_size_ < 0)
325+
throw std::runtime_error("LinearSolver.factorize(A) must be called before solve(b)");
326+
if (b.size() != factorized_size_)
327+
{
328+
throw std::runtime_error(
329+
"right-hand side has size " + std::to_string(b.size()) + ", expected "
330+
+ std::to_string(factorized_size_));
331+
}
332+
333+
TVector x;
334+
if (x0_obj.is_none())
335+
{
336+
x = TVector::Zero(b.size());
337+
}
338+
else
339+
{
340+
x = x0_obj.cast<TVector>();
341+
if (x.size() != b.size())
342+
{
343+
throw std::runtime_error(
344+
"initial guess has size " + std::to_string(x.size()) + ", expected "
345+
+ std::to_string(b.size()));
346+
}
347+
}
348+
349+
py::scoped_ostream_redirect stdout_redirect(
350+
std::cout, py::module_::import("sys").attr("stdout"));
351+
{
352+
py::gil_scoped_release release;
353+
solver_->solve(b, x);
354+
}
355+
return x;
356+
}
357+
358+
polysolve::json info() const
359+
{
360+
polysolve::json result;
361+
solver_->get_info(result);
362+
return result;
363+
}
364+
365+
std::string name() const { return solver_->name(); }
366+
bool is_dense() const { return solver_->is_dense(); }
367+
368+
private:
369+
std::shared_ptr<spdlog::logger> logger_;
370+
std::unique_ptr<polysolve::linear::Solver> solver_;
371+
Eigen::Index factorized_size_ = -1;
372+
};
373+
208374
} // namespace
209375

210376
PYBIND11_MODULE(polysolve, m)
211377
{
212378
using namespace polysolve;
213379
namespace nonlinear = polysolve::nonlinear;
380+
namespace linear = polysolve::linear;
214381
namespace nl = nlohmann;
215382

216-
m.doc() = "Python bindings for PolySolve nonlinear optimization.";
383+
m.doc() = "Python bindings for PolySolve nonlinear optimization and linear solvers.";
217384

218385
py::enum_<spdlog::level::level_enum>(m, "LogLevel")
219386
.value("trace", spdlog::level::trace)
@@ -240,6 +407,109 @@ PYBIND11_MODULE(polysolve, m)
240407
return hessian;
241408
});
242409

410+
py::class_<LinearSolver>(m, "LinearSolver")
411+
.def(
412+
py::init<const py::dict &, spdlog::level::level_enum, bool>(),
413+
"Create a reusable PolySolve linear solver.",
414+
py::arg("solver_params") = py::dict(),
415+
py::arg("log_level") = spdlog::level::info,
416+
py::arg("strict_validation") = true)
417+
.def(
418+
"set_parameters",
419+
&LinearSolver::set_parameters,
420+
"Set linear solver parameters.",
421+
py::arg("params"))
422+
.def(
423+
"analyze_pattern",
424+
&LinearSolver::analyze_pattern,
425+
"Analyze the system matrix sparsity pattern.",
426+
py::arg("A"),
427+
py::arg("precond_num") = -1)
428+
.def(
429+
"analyse_pattern",
430+
&LinearSolver::analyze_pattern,
431+
"Analyze the system matrix sparsity pattern.",
432+
py::arg("A"),
433+
py::arg("precond_num") = -1)
434+
.def(
435+
"factorize",
436+
&LinearSolver::factorize,
437+
"Factorize the system matrix.",
438+
py::arg("A"))
439+
.def(
440+
"factorise",
441+
&LinearSolver::factorize,
442+
"Factorize the system matrix.",
443+
py::arg("A"))
444+
.def(
445+
"solve",
446+
&LinearSolver::solve,
447+
"Solve the factorized linear system.",
448+
py::arg("b"),
449+
py::arg("x0") = py::none())
450+
.def(
451+
"info",
452+
&LinearSolver::info,
453+
"Return information from the last solve.")
454+
.def_property_readonly("name", &LinearSolver::name)
455+
.def_property_readonly("is_dense", &LinearSolver::is_dense)
456+
.def_static(
457+
"available_solvers",
458+
[]() { return linear::Solver::available_solvers(); },
459+
"List available linear solvers.")
460+
.def_static(
461+
"default_solver",
462+
[]() { return linear::Solver::default_solver(); },
463+
"Return the default linear solver.")
464+
.def_static(
465+
"available_preconditioners",
466+
[]() { return linear::Solver::available_preconds(); },
467+
"List available linear solver preconditioners.")
468+
.def_static(
469+
"default_preconditioner",
470+
[]() { return linear::Solver::default_precond(); },
471+
"Return the default linear solver preconditioner.");
472+
473+
m.def(
474+
"solve",
475+
[](const py::object &A,
476+
const TVector &b,
477+
const py::dict &solver_params,
478+
const py::object &x0,
479+
const int precond_num,
480+
const spdlog::level::level_enum log_level,
481+
const bool strict_validation) {
482+
LinearSolver solver(solver_params, log_level, strict_validation);
483+
solver.analyze_pattern(A, precond_num);
484+
solver.factorize(A);
485+
return solver.solve(b, x0);
486+
},
487+
"Solve a linear system Ax = b.",
488+
py::arg("A"),
489+
py::arg("b"),
490+
py::arg("solver_params") = py::dict(),
491+
py::arg("x0") = py::none(),
492+
py::arg("precond_num") = -1,
493+
py::arg("log_level") = spdlog::level::info,
494+
py::arg("strict_validation") = true);
495+
496+
m.def(
497+
"available_linear_solvers",
498+
[]() { return linear::Solver::available_solvers(); },
499+
"List available linear solvers.");
500+
m.def(
501+
"default_linear_solver",
502+
[]() { return linear::Solver::default_solver(); },
503+
"Return the default linear solver.");
504+
m.def(
505+
"available_linear_preconditioners",
506+
[]() { return linear::Solver::available_preconds(); },
507+
"List available linear solver preconditioners.");
508+
m.def(
509+
"default_linear_preconditioner",
510+
[]() { return linear::Solver::default_precond(); },
511+
"Return the default linear solver preconditioner.");
512+
243513
m.def(
244514
"minimize",
245515
[](Problem &problem,

0 commit comments

Comments
 (0)