diff --git a/_pyceres/core/cost_functions.h b/_pyceres/core/cost_functions.h index ba64d0c..0eb4b5b 100644 --- a/_pyceres/core/cost_functions.h +++ b/_pyceres/core/cost_functions.h @@ -17,6 +17,20 @@ class PyCostFunction : public ceres::CostFunction, using ceres::CostFunction::CostFunction; using ceres::CostFunction::set_num_residuals; + void set_use_numeric_diff(bool use_numeric_diff) { + use_numeric_diff_ = use_numeric_diff; + } + + bool use_numeric_diff() const { return use_numeric_diff_; } + + void set_numeric_diff_method(ceres::NumericDiffMethodType method) { + numeric_diff_method_ = method; + } + + ceres::NumericDiffMethodType numeric_diff_method() const { + return numeric_diff_method_; + } + bool Evaluate(double const* const* parameters, double* residuals, double** jacobians) const override { @@ -26,17 +40,31 @@ class PyCostFunction : public ceres::CostFunction, // pointer values if (!cached_flag) { parameters_vec.reserve(this->parameter_block_sizes().size()); - jacobians_vec.reserve(this->parameter_block_sizes().size()); residuals_wrap = py::array_t(num_residuals(), residuals, no_copy); for (size_t idx = 0; idx < parameter_block_sizes().size(); ++idx) { parameters_vec.emplace_back(py::array_t( this->parameter_block_sizes()[idx], parameters[idx], no_copy)); + } + if (jacobians) { + jacobians_vec.reserve(this->parameter_block_sizes().size()); + for (size_t idx = 0; idx < parameter_block_sizes().size(); ++idx) { + jacobians_vec.emplace_back(py::array_t( + this->parameter_block_sizes()[idx] * num_residuals(), + jacobians[idx], + no_copy)); + } + cached_jacobians_flag = true; + } + cached_flag = true; + } else if (jacobians && !cached_jacobians_flag) { + jacobians_vec.reserve(this->parameter_block_sizes().size()); + for (size_t idx = 0; idx < parameter_block_sizes().size(); ++idx) { jacobians_vec.emplace_back(py::array_t( this->parameter_block_sizes()[idx] * num_residuals(), jacobians[idx], no_copy)); } - cached_flag = true; + cached_jacobians_flag = true; } // Check if the pointers have changed and if they have then change them @@ -51,7 +79,7 @@ class PyCostFunction : public ceres::CostFunction, this->parameter_block_sizes()[idx], parameters[idx], no_copy); } } - if (jacobians) { + if (jacobians && cached_jacobians_flag) { info = jacobians_vec[0].request(true); if (info.ptr != jacobians) { for (size_t idx = 0; idx < jacobians_vec.size(); ++idx) { @@ -85,10 +113,15 @@ class PyCostFunction : public ceres::CostFunction, mutable std::vector> jacobians_vec; // Flag used to determine if the vectors need to be resized. mutable bool cached_flag = false; + mutable bool cached_jacobians_flag = false; // Buffer to contain the residuals pointer. mutable py::array_t residuals_wrap; // Dummy variable for pybind11 to avoid a copy. mutable py::str no_copy; + + bool use_numeric_diff_ = false; + ceres::NumericDiffMethodType numeric_diff_method_ = + ceres::NumericDiffMethodType::CENTRAL; }; void BindCostFunctions(py::module& m) { @@ -104,6 +137,32 @@ void BindCostFunctions(py::module& m) { &ceres::CostFunction::parameter_block_sizes, py::return_value_policy::reference) .def("set_num_residuals", &PyCostFunction::set_num_residuals) + .def( + "set_use_numeric_diff", + [](ceres::CostFunction& self, bool use_numeric_diff) { + auto* py_cost = dynamic_cast(&self); + THROW_CHECK(py_cost); + py_cost->set_use_numeric_diff(use_numeric_diff); + }, + py::arg("use_numeric_diff") = true) + .def("use_numeric_diff", [](ceres::CostFunction& self) { + auto* py_cost = dynamic_cast(&self); + THROW_CHECK(py_cost); + return py_cost->use_numeric_diff(); + }) + .def( + "set_numeric_diff_method", + [](ceres::CostFunction& self, ceres::NumericDiffMethodType method) { + auto* py_cost = dynamic_cast(&self); + THROW_CHECK(py_cost); + py_cost->set_numeric_diff_method(method); + }, + py::arg("method")) + .def("numeric_diff_method", [](ceres::CostFunction& self) { + auto* py_cost = dynamic_cast(&self); + THROW_CHECK(py_cost); + return py_cost->numeric_diff_method(); + }) .def("set_parameter_block_sizes", [](ceres::CostFunction& myself, std::vector& sizes) { for (auto s : sizes) { diff --git a/_pyceres/core/problem.h b/_pyceres/core/problem.h index 71eb4ce..6817102 100644 --- a/_pyceres/core/problem.h +++ b/_pyceres/core/problem.h @@ -1,5 +1,6 @@ #pragma once +#include "_pyceres/core/cost_functions.h" #include "_pyceres/core/wrappers.h" #include "_pyceres/helpers.h" #include "_pyceres/logging.h" @@ -23,6 +24,51 @@ void SetResidualBlocks( } } +class PyCostFunctionResidualFunctor { + public: + explicit PyCostFunctionResidualFunctor(py::object cost_obj) + : cost_obj_(std::move(cost_obj)), + cost_(cost_obj_.cast()) {} + + bool operator()(double const* const* parameters, double* residuals) const { + return cost_->Evaluate(parameters, residuals, nullptr); + } + + private: + py::object cost_obj_; + PyCostFunction* cost_; +}; + +template +ceres::CostFunction* MakeNumericDiffCostFunction(PyCostFunction* cost, + const py::object& cost_obj) { + auto* functor = new PyCostFunctionResidualFunctor(cost_obj); + auto* diff_cost = new ceres::DynamicNumericDiffCostFunction< + PyCostFunctionResidualFunctor, + kMethod>(functor); + const auto& sizes = cost->parameter_block_sizes(); + for (int size : sizes) { + diff_cost->AddParameterBlock(size); + } + diff_cost->SetNumResiduals(cost->num_residuals()); + return diff_cost; +} + +ceres::CostFunction* CreateNumericDiffCostFunction(PyCostFunction* cost, + const py::object& cost_obj) { + switch (cost->numeric_diff_method()) { + case ceres::NumericDiffMethodType::FORWARD: + return MakeNumericDiffCostFunction( + cost, cost_obj); + case ceres::NumericDiffMethodType::CENTRAL: + return MakeNumericDiffCostFunction( + cost, cost_obj); + default: + return MakeNumericDiffCostFunction( + cost, cost_obj); + } +} + } // namespace // Function to create Problem::Options with DO_NOT_TAKE_OWNERSHIP @@ -218,7 +264,15 @@ void BindProblem(py::module& m) { } THROW_CHECK_EQ(num_dims, cost->parameter_block_sizes()[i]); } - ceres::CostFunction* costw = new CostFunctionWrapper(cost); + ceres::CostFunction* costw = nullptr; + auto* py_cost = dynamic_cast(cost); + if (py_cost && py_cost->use_numeric_diff()) { + py::object cost_obj = + py::cast(cost, py::return_value_policy::reference); + costw = CreateNumericDiffCostFunction(py_cost, cost_obj); + } else { + costw = new CostFunctionWrapper(cost); + } return ResidualBlockIDWrapper( self.AddResidualBlock(costw, loss.get(), pointer_values)); }, diff --git a/_pyceres/core/types.h b/_pyceres/core/types.h index b9c4fe7..fb90407 100644 --- a/_pyceres/core/types.h +++ b/_pyceres/core/types.h @@ -159,4 +159,10 @@ void BindTypes(py::module& m) { .value("USER_SUCCESS", ceres::TerminationType::USER_SUCCESS) .value("USER_FAILURE", ceres::TerminationType::USER_FAILURE); AddStringToEnumConstructor(termt); + + auto ndmt = + py::enum_(m, "NumericDiffMethodType") + .value("FORWARD", ceres::NumericDiffMethodType::FORWARD) + .value("CENTRAL", ceres::NumericDiffMethodType::CENTRAL); + AddStringToEnumConstructor(ndmt); } diff --git a/examples/curve_fitting.py b/examples/curve_fitting.py new file mode 100644 index 0000000..d9d2e47 --- /dev/null +++ b/examples/curve_fitting.py @@ -0,0 +1,93 @@ +import numpy as np +import pyceres +import matplotlib.pyplot as plt +import math + + +class ExpResidual(pyceres.CostFunction): + def __init__(self, x, y_obs): + super().__init__() + self.x = x + self.y_obs = y_obs + self.set_num_residuals(1) + self.set_parameter_block_sizes([1, 1]) + + + + def Evaluate(self, parameters, residuals, jacobians): + m = parameters[0][0] + c = parameters[1][0] + + # Model prediction + y_pred = math.exp(m * self.x + c) + + # Residual: r = y_pred - y_obs + residuals[0] = y_pred - self.y_obs + + if jacobians is not None: + # ∂r/∂m = x * exp(m x + c) + if jacobians[0] is not None: + jacobians[0][0] = self.x * y_pred + + # ∂r/∂c = exp(m x + c) + if jacobians[1] is not None: + jacobians[1][0] = y_pred + + return True + + + +def main(): + # True parameters + m_true = 0.3 + c_true = 0.1 + sigma = 0.4 + + # Generate data + x = np.linspace(-5, 5, 400) + y_clean = np.exp(m_true * x + c_true) + noise = np.random.normal(0.0, sigma, size=x.shape) + y_noisy = y_clean + noise + + # Initial guesses (what Ceres will optimize) + m_est = np.array([0.0], dtype=np.float64) + c_est = np.array([0.0], dtype=np.float64) + + problem = pyceres.Problem() + + # One residual per data point + for px, py in zip(x, y_noisy): + problem.add_residual_block( + ExpResidual(px, py), + None, + [m_est, c_est] + ) + + # Solve + options = pyceres.SolverOptions() + options.linear_solver_type = pyceres.LinearSolverType.DENSE_NORMAL_CHOLESKY + options.max_num_iterations = 10000 + options.minimizer_progress_to_stdout = False + + summary = pyceres.SolverSummary() + pyceres.solve(options, problem, summary) + + print(summary.BriefReport()) + print("Estimated m, c:", m_est[0], c_est[0]) + + # Plot results + y_fit = np.exp(m_est[0] * x + c_est[0]) + + plt.figure() + plt.scatter(x, y_noisy, s=10, alpha=0.5, label="Noisy data") + plt.plot(x, y_fit, "r", linewidth=2, label="Fitted curve") + plt.plot(x, y_clean, "k--", label="True curve") + plt.xlabel("x") + plt.ylabel("y") + plt.legend() + plt.grid(True) + plt.show() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/curve_fitting_autodiff.py b/examples/curve_fitting_autodiff.py new file mode 100644 index 0000000..29ae4b2 --- /dev/null +++ b/examples/curve_fitting_autodiff.py @@ -0,0 +1,94 @@ +import numpy as np +import pyceres +import matplotlib.pyplot as plt +import math + + +class ExpResidual(pyceres.CostFunction): + def __init__(self, x, y_obs): + super().__init__() + self.x = x + self.y_obs = y_obs + self.set_num_residuals(1) + self.set_parameter_block_sizes([1, 1]) + self.set_use_numeric_diff(True) + + + + def Evaluate(self, parameters, residuals, jacobians): + m = parameters[0][0] + c = parameters[1][0] + + # Model prediction + y_pred = math.exp(m * self.x + c) + + # Residual: r = y_pred - y_obs + residuals[0] = y_pred - self.y_obs + + # if jacobians is not None: + # # ∂r/∂m = x * exp(m x + c) + # if jacobians[0] is not None: + # jacobians[0][0] = self.x * y_pred + + # # ∂r/∂c = exp(m x + c) + # if jacobians[1] is not None: + # jacobians[1][0] = y_pred + + return True + + + +def main(): + # True parameters + m_true = 0.3 + c_true = 0.1 + sigma = 0.4 + + # Generate data + x = np.linspace(-5, 5, 400) + y_clean = np.exp(m_true * x + c_true) + noise = np.random.normal(0.0, sigma, size=x.shape) + y_noisy = y_clean + noise + + # Initial guesses (what Ceres will optimize) + m_est = np.array([0.0], dtype=np.float64) + c_est = np.array([0.0], dtype=np.float64) + + problem = pyceres.Problem() + + # One residual per data point + for px, py in zip(x, y_noisy): + problem.add_residual_block( + ExpResidual(px, py), + None, + [m_est, c_est] + ) + + # Solve + options = pyceres.SolverOptions() + options.linear_solver_type = pyceres.LinearSolverType.DENSE_NORMAL_CHOLESKY + options.max_num_iterations = 10000 + options.minimizer_progress_to_stdout = False + + summary = pyceres.SolverSummary() + pyceres.solve(options, problem, summary) + + print(summary.BriefReport()) + print("Estimated m, c:", m_est[0], c_est[0]) + + # Plot results + y_fit = np.exp(m_est[0] * x + c_est[0]) + + plt.figure() + plt.scatter(x, y_noisy, s=10, alpha=0.5, label="Noisy data") + plt.plot(x, y_fit, "r", linewidth=2, label="Fitted curve") + plt.plot(x, y_clean, "k--", label="True curve") + plt.xlabel("x") + plt.ylabel("y") + plt.legend() + plt.grid(True) + plt.show() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/examples/hello.py b/examples/hello.py new file mode 100644 index 0000000..a19b2f7 --- /dev/null +++ b/examples/hello.py @@ -0,0 +1,51 @@ +import numpy as np +import pyceres + + +# Cost functor equivalent to C++ AutoDiffCostFunction +class CostFunctor(pyceres.CostFunction): + def __init__(self): + super().__init__() + self.set_num_residuals(1) + self.set_parameter_block_sizes([1]) + + def Evaluate(self, parameters, residuals, jacobians): + x = parameters[0][0] + residuals[0] = 10.0 - x + + if(jacobians != None): + jacobians[0][:] = -1 + + # Let pyceres do numeric differentiation + return True + + +def main(): + # Initial value + initial_x = 30000000 + x = np.array([initial_x], dtype=np.float64) + + # Build problem + problem = pyceres.Problem() + + cost_function = CostFunctor() + problem.add_residual_block( + cost_function, + None, # no loss function + [x] + ) + + # Solver options + options = pyceres.SolverOptions() + options.linear_solver_type = pyceres.LinearSolverType.DENSE_QR + options.minimizer_progress_to_stdout = True + + summary = pyceres.SolverSummary() + pyceres.solve(options, problem, summary) + + print(summary.BriefReport()) + print(f"x : {initial_x} -> {x[0]}") + + +if __name__ == "__main__": + main() diff --git a/examples/powells.py b/examples/powells.py new file mode 100644 index 0000000..61164e2 --- /dev/null +++ b/examples/powells.py @@ -0,0 +1,117 @@ + +import numpy as np +import pyceres +import math + + +class F1(pyceres.CostFunction): + def __init__(self): + super().__init__() + self.set_num_residuals(1) + self.set_parameter_block_sizes([1, 1]) + + def Evaluate(self, parameters, residuals, jacobians): + x1 = parameters[0][0] + x2 = parameters[1][0] + residuals[0] = x1 + 10.0 * x2 + if jacobians is not None: + if jacobians[0] is not None: + jacobians[0][0] = 1.0 + if jacobians[1] is not None: + jacobians[1][0] = 10.0 + return True + + +class F2(pyceres.CostFunction): + def __init__(self): + super().__init__() + self.set_num_residuals(1) + self.set_parameter_block_sizes([1, 1]) + + def Evaluate(self, parameters, residuals, jacobians): + x3 = parameters[0][0] + x4 = parameters[1][0] + residuals[0] = math.sqrt(5.0) * (x3 - x4) + s = math.sqrt(5.0) + + if jacobians is not None: + if jacobians[0] is not None: + jacobians[0][0] = s + if jacobians[1] is not None: + jacobians[1][0] = -s + return True + + +class F3(pyceres.CostFunction): + def __init__(self): + super().__init__() + self.set_num_residuals(1) + self.set_parameter_block_sizes([1, 1]) + + def Evaluate(self, parameters, residuals, jacobians): + x2 = parameters[0][0] + x3 = parameters[1][0] + r = x2 - 2.0 * x3 + r = x2 - 2.0 * x3 + residuals[0] = r * r + + if jacobians is not None: + if jacobians[0] is not None: + jacobians[0][0] = 2.0 * r + if jacobians[1] is not None: + jacobians[1][0] = -4.0 * r + return True + + +class F4(pyceres.CostFunction): + def __init__(self): + super().__init__() + self.set_num_residuals(1) + self.set_parameter_block_sizes([1, 1]) + + def Evaluate(self, parameters, residuals, jacobians): + x1 = parameters[0][0] + x4 = parameters[1][0] + r = x1 - x4 + residuals[0] = math.sqrt(10.0) * r * r + r = 2*math.sqrt(10.0)*r + + if(jacobians != None): + jacobians[0][0] = r + jacobians[1][0] = -r + return True + + +def main(): + # Initial values (SCALARS, just like Ceres Example) + x1 = np.array([3.0], dtype=np.float64) + x2 = np.array([-1.0], dtype=np.float64) + x3 = np.array([0.0], dtype=np.float64) + x4 = np.array([1.0], dtype=np.float64) + + x0 = (x1.copy(), x2.copy(), x3.copy(), x4.copy()) + + problem = pyceres.Problem() + + # Add residual blocks + problem.add_residual_block(F1(), None, [x1, x2]) + problem.add_residual_block(F2(), None, [x3, x4]) + problem.add_residual_block(F3(), None, [x2, x3]) + problem.add_residual_block(F4(), None, [x1, x4]) + + options = pyceres.SolverOptions() + options.linear_solver_type = pyceres.LinearSolverType.DENSE_QR + options.max_num_iterations = 100 + options.minimizer_progress_to_stdout = True + + summary = pyceres.SolverSummary() + pyceres.solve(options, problem, summary) + + print(summary.BriefReport()) + print("Initial:", [v[0] for v in x0]) + print("Final: ", [x1[0], x2[0], x3[0], x4[0]]) + + +if __name__ == "__main__": + main() + diff --git a/examples/powells_autodiff.py b/examples/powells_autodiff.py new file mode 100644 index 0000000..9629c4a --- /dev/null +++ b/examples/powells_autodiff.py @@ -0,0 +1,93 @@ +import math +import numpy as np +import pyceres + + +class F1(pyceres.CostFunction): + def __init__(self): + super().__init__() + self.set_num_residuals(1) + self.set_parameter_block_sizes([1, 1]) + self.set_use_numeric_diff(True) + + def Evaluate(self, parameters, residuals, jacobians): + x1 = parameters[0][0] + x2 = parameters[1][0] + residuals[0] = x1 + 10.0 * x2 + return True + + +class F2(pyceres.CostFunction): + def __init__(self): + super().__init__() + self.set_num_residuals(1) + self.set_parameter_block_sizes([1, 1]) + self.set_use_numeric_diff(True) + + def Evaluate(self, parameters, residuals, jacobians): + x3 = parameters[0][0] + x4 = parameters[1][0] + residuals[0] = math.sqrt(5.0) * (x3 - x4) + return True + + +class F3(pyceres.CostFunction): + def __init__(self): + super().__init__() + self.set_num_residuals(1) + self.set_parameter_block_sizes([1, 1]) + self.set_use_numeric_diff(True) + + def Evaluate(self, parameters, residuals, jacobians): + x2 = parameters[0][0] + x3 = parameters[1][0] + r = x2 - 2.0 * x3 + residuals[0] = r * r + return True + + +class F4(pyceres.CostFunction): + def __init__(self): + super().__init__() + self.set_num_residuals(1) + self.set_parameter_block_sizes([1, 1]) + self.set_use_numeric_diff(True) + + def Evaluate(self, parameters, residuals, jacobians): + x1 = parameters[0][0] + x4 = parameters[1][0] + r = x1 - x4 + residuals[0] = math.sqrt(10.0) * r * r + return True + + +def main(): + x1 = np.array([3.0], dtype=np.float64) + x2 = np.array([-1.0], dtype=np.float64) + x3 = np.array([0.0], dtype=np.float64) + x4 = np.array([1.0], dtype=np.float64) + + x0 = (x1.copy(), x2.copy(), x3.copy(), x4.copy()) + + problem = pyceres.Problem() + + problem.add_residual_block(F1(), None, [x1, x2]) + problem.add_residual_block(F2(), None, [x3, x4]) + problem.add_residual_block(F3(), None, [x2, x3]) + problem.add_residual_block(F4(), None, [x1, x4]) + + options = pyceres.SolverOptions() + options.linear_solver_type = pyceres.LinearSolverType.DENSE_QR + options.max_num_iterations = 100 + options.minimizer_progress_to_stdout = True + + summary = pyceres.SolverSummary() + pyceres.solve(options, problem, summary) + + print(summary.BriefReport()) + print("Initial:", [v[0] for v in x0]) + print("Final: ", [x1[0], x2[0], x3[0], x4[0]]) + + +if __name__ == "__main__": + main() diff --git a/examples/robust_curve_fitting_loss.py b/examples/robust_curve_fitting_loss.py new file mode 100644 index 0000000..5e6c0dd --- /dev/null +++ b/examples/robust_curve_fitting_loss.py @@ -0,0 +1,118 @@ +import numpy as np +import pyceres +import matplotlib.pyplot as plt +import math + + +class ExpResidual(pyceres.CostFunction): + def __init__(self, x, y_obs): + super().__init__() + self.x = x + self.y_obs = y_obs + self.set_num_residuals(1) + self.set_parameter_block_sizes([1, 1]) + + + + def Evaluate(self, parameters, residuals, jacobians): + m = parameters[0][0] + c = parameters[1][0] + + # Model prediction + y_pred = math.exp(m * self.x + c) + + # Residual: r = y_pred - y_obs + residuals[0] = y_pred - self.y_obs + + if jacobians is not None: + # ∂r/∂m = x * exp(m x + c) + if jacobians[0] is not None: + jacobians[0][0] = self.x * y_pred + + # ∂r/∂c = exp(m x + c) + if jacobians[1] is not None: + jacobians[1][0] = y_pred + + return True + + + +def main(): + # True parameters + m_true = 0.3 + c_true = 0.1 + sigma = 0.4 + + #Outlier noise + sigma2 = 10.0 + outlier_prob = 0.1 + + # Generate data + x = np.linspace(-5, 5, 400) + y_clean = np.exp(m_true * x + c_true) + noise = np.random.normal(0.0, sigma, size=x.shape) + outliers = np.random.rand(len(x)) < outlier_prob + noise[outliers] += np.random.normal(0.0, sigma2, size=np.sum(outliers)) + + + y_noisy = y_clean + noise + + # Initial guesses (what Ceres will optimize) + m_est_no_loss = np.array([0.0], dtype=np.float64) + c_est_no_loss = np.array([0.0], dtype=np.float64) + + m_est_loss = np.array([0.0], dtype=np.float64) + c_est_loss = np.array([0.0], dtype=np.float64) + + problem_no_loss = pyceres.Problem() + problem_loss = pyceres.Problem() + + # One residual per data point + for px, py in zip(x, y_noisy): + problem_no_loss.add_residual_block( + ExpResidual(px, py), + None, + [m_est_no_loss, c_est_no_loss] + ) + + for px, py in zip(x, y_noisy): + problem_loss.add_residual_block( + ExpResidual(px, py), + pyceres.CauchyLoss(0.5), + [m_est_loss, c_est_loss] + ) + + # Solve + options = pyceres.SolverOptions() + options.linear_solver_type = pyceres.LinearSolverType.DENSE_NORMAL_CHOLESKY + options.max_num_iterations = 50 + options.minimizer_progress_to_stdout = False + + summary_no_loss = pyceres.SolverSummary() + pyceres.solve(options, problem_no_loss, summary_no_loss) + summary_loss = pyceres.SolverSummary() + pyceres.solve(options, problem_loss, summary_loss) + + print(summary_no_loss.BriefReport()) + + print("Estimated m, c:", m_est_no_loss[0], c_est_no_loss[0]) + print("Estimated m, c:", m_est_loss[0], c_est_loss[0]) + + # Plot results + y_fit1 = np.exp(m_est_no_loss[0] * x + c_est_no_loss[0]) + y_fit2 = np.exp(m_est_loss[0] * x + c_est_loss[0]) + + plt.figure() + plt.scatter(x, y_noisy, s=10, alpha=0.5, label="Noisy data") + plt.plot(x, y_fit1, "r", linewidth=2, label="Simple Fitted Curve") + plt.plot(x, y_fit2, "b", linewidth=2, label="Robust Fitted Curve") + plt.plot(x, y_clean, "k--", label="True curve") + plt.xlabel("x") + plt.ylabel("y") + plt.legend() + plt.grid(True) + plt.show() + + +if __name__ == "__main__": + main() \ No newline at end of file