//  [python]
def trf_bounds(self, func, jac, x0, f0, J0, lb, ub, 
                   ftol, xtol, gtol, max_nfev, loss_function):
        """"""
        x = x0.clone()
        f = f0
        f_true = f.clone()
        nfev = 1

        J = J0
        njev = 1
        m, n = J.shape
        
        if loss_function is not None:
            rho = loss_function(f)
            cost = 0.5 * torch.sum(rho[0])
            J, f = self.scale_for_robust_loss_function(J, f, rho)
        else:
            cost = 0.5 * torch.dot(f, f)

        g = torch.einsum('ij, j->i', J.T, f) # J.T.dot(f) calculate the grad
        
        # before update, we need to compute variables scale based on the jacobian 
        scale_inv = torch.sum(J**2, dim=0) ** 0.5
        scale_inv[scale_inv == 0] = 1
        scale = 1 / scale_inv

        # compute the scaling vector and its derivatives
        v, dv = self.CL_scaling_vector(x, g, lb, ub)
        v[dv != 0] *= scale_inv[dv != 0]
        Delta = torch.norm(x0 * scale_inv / v**0.5)
        if Delta == 0:
            Delta = torch.tensor(1., dtype=self.dtype, device=self.device)

        g_norm = torch.max(torch.abs(g * v))

        f_augmented = torch.zeros(m+n, dtype=self.dtype, device=self.device)
        J_augmented = torch.empty((m+n, n), dtype=self.dtype, device=self.device) # augment for exact solver

        if max_nfev is None:
            max_nfev = n * 100 # 100 multiply the size of x0

        alpha = 0.0 # "Levenberg-Marquardt" parameter
        termination_status = None
        iteration = 0
        step_norm = None
        actual_reduction = None

        while True:
            v, dv = self.CL_scaling_vector(x, g, lb, ub)

            g_norm = torch.max(torch.abs(g))
            if g_norm < gtol:
                termination_status = 1

            if termination_status is not None or nfev == max_nfev:
                break
            
            # v is recomputed in the variables after applying `x_scale`, note that
            # components which were identically 1 not affected.
            v[dv != 0] *= scale_inv[dv != 0]

            d = v ** 0.5 * scale # here, two type of scaling is applied

            # C = diag(g * scale) Jv
            diag_h = g * dv * scale

            # "hat" gradient
            g_h = d * g
            
            # similar to the svd in no bounds
            f_augmented[:m] = f
            J_augmented[:m] = J * d
            J_h = J_augmented[:m]  # Memory view.
            J_augmented[m:] = torch.diag(diag_h**0.5)
            U, s, V = torch.linalg.svd(J_augmented, full_matrices=False)
            V = V.T
            uf = torch.einsum('ij, j->i', U.T, f_augmented)

            # theta controls step back step ratio from the bounds
            theta = max(0.995, 1 - g_norm)

            actual_reduction = -1
            while actual_reduction <= 0 and nfev <= max_nfev:
                # Solve a trust-region problem in least-squares minimization
                p_h, alpha, n_iter = self.solve_lsq_trust_region(
                    n, m, uf, s, V, Delta, initial_alpha=alpha
                )

                p = d * p_h  # Trust-region solution in the original space.
                step, step_h, predicted_reduction = self.select_step(
                    x, J_h, diag_h, g_h, p, p_h, d, Delta, lb, ub, theta)

                x_new = self.make_strictly_feasible(x + step, lb, ub, rstep=0)
                f_new = func(x_new)
                nfev += 1

                step_h_norm = torch.norm(step_h)

                if not torch.all(torch.isfinite(f_new)):
                    Delta = 0.25 * step_h_norm
                    continue    

                cost_new = 0.5 * torch.dot(f_new, f_new)
                actual_reduction = cost - cost_new

                Delta_new, ratio = self.update_tr_radius(
                    Delta, actual_reduction, predicted_reduction,
                    step_h_norm, step_h_norm > 0.95 * Delta)
                
                step_norm = torch.norm(step)
                termination_status = self.check_termination(
                    actual_reduction, cost, step_norm, torch.norm(x), ratio, ftol, xtol)

                if termination_status is not None:
                    break
                
                alpha *= Delta / Delta_new
                Delta = Delta_new

            # check the validation of solution if actual reduction larger than 0
            if actual_reduction > 0:
                x = x_new
                f = f_new
                f_true = f.clone()

                cost = cost_new

                J = jac(x, f)
                njev += 1

                if loss_function is not None:
                    rho = loss_function(f)
                    J, f = self.scale_for_robust_loss_function(J, f, rho)

                g = torch.einsum('ij, j->i', J.T, f)

                scale_inv = torch.sum(J**2, dim=0) ** 0.5
                scale_inv[scale_inv == 0] = 1
                scale = 1 / scale_inv
                
                # save the system configuration
                self.system.save_to_json('./lens_file/' + self.system.LensName +
                                         datetime.now().strftime('_%y%m%d_%H%M%S') + 
                                         '.json')

            else:
                step_norm = 0
                actual_reduction = 0

            # return the optmized solution in this situation
            print('<iter>: {:4d}, merit func norm: {:.8e}, cost: {:.8e}, first-order optimality {:.4e}, termination status: {}'
                  .format(iteration, torch.norm(f_true).item(), cost.item(), 
                          g_norm.item(), termination_status))
            iteration += 1

        return {'nfev': nfev, 'cost': cost.item(), 'optimality': g_norm.item()}