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 ¶ms)
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
210376PYBIND11_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