Files
calibration/rtk_imu/rtk_imu_node_graph.py
T

530 lines
24 KiB
Python

'''Per-GNSS-node RTK/IMU state graph used after legacy propagation deprecation.'''
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
from scipy.optimize import least_squares
from scipy.sparse import lil_matrix
from imu_lidar.geometry import so3_exp, so3_log
from imu_lidar.imu_preintegration import apply_bias_correction_imu, residual_whiten_matrix
from .rtk_imu_engineering import (
G_ENU, HPR_DIRECT_ANGULAR_SIGMA_RAD, _Segment, _world_rtk)
NODE_DOF = 15
SIGMA_BG_RW = 1e-5
SIGMA_BA_RW = 1e-3
@dataclass(frozen=True)
class NodeGraphProblem:
segment: _Segment
R_seed_WI: tuple[np.ndarray,...]
fixed_l_I_m: np.ndarray
R_RTK_IMU: np.ndarray
hpr_direct_angular_sigma_rad: float = HPR_DIRECT_ANGULAR_SIGMA_RAD
@dataclass(frozen=True)
class NodeGraphResult:
success: bool
message: str
node_count: int
duration_s: float
fixed_l_I_m: np.ndarray
initial_cost: float
final_cost: float
cost_reduction: float
nfev: int
optimality: float
gradient_norm: float
residual_dimension: int
state_dimension: int
statistical_dof: int
total_nis: float
chi_square_per_dof: float
cost_per_dof: float
initial_residual_by_factor: dict[str,dict[str,float]]
final_residual_by_factor: dict[str,dict[str,float]]
final_position_residual_m: dict[str,object]
final_velocity_residual_m_s: dict[str,object]
max_bg_step_rad_s: float
max_ba_step_m_s2: float
preintegration_covariance_sigma: dict[str,dict[str,float]]
@dataclass(frozen=True)
class FreeLeverResult:
success: bool
message: str
initial_l_I_m: np.ndarray
final_l_I_m: np.ndarray
lever_step_norm_m: float
initial_cost: float
final_cost: float
nfev: int
optimality: float
chi_square_per_dof: float
position_residual_m: dict[str,object]
velocity_residual_m_s: dict[str,object]
residual_by_factor: dict[str,dict[str,float]]
lever_covariance_m2: np.ndarray
lever_information_singular_values: np.ndarray
lever_information_condition_number: float
lever_precision_rank: int
weakest_lever_direction_I: np.ndarray
def _rotation(problem,index,x):
offset = NODE_DOF*index
return problem.R_seed_WI[index] @ so3_exp(x[offset:offset+3])
def initial_parameters(problem, lever_override=None):
nodes = problem.segment.nodes
x = np.zeros(NODE_DOF*len(nodes))
lever = (problem.fixed_l_I_m if lever_override is None
else np.asarray(lever_override,dtype=float))
previous_p, previous_v = np.zeros(3), np.zeros(3)
for index,node in enumerate(nodes):
offset = NODE_DOF*index
R = problem.R_seed_WI[index]
previous_p = node.p_enu_m-R@lever
if index and not node.position_mask[2]:
previous_p[2] = x[offset-NODE_DOF+5]
if node.velocity_enu_m_s is not None:
previous_v = node.velocity_enu_m_s-R@np.cross(node.gyro_rad_s,lever)
x[offset+3:offset+6] = previous_p
x[offset+6:offset+9] = previous_v
return x
def _stats(values, effective_dof=None):
a = np.asarray(values,dtype=float).reshape(-1)
if not a.size:
return {'count':0,'dof':0,'rms':np.nan,'p50_abs':np.nan,
'p95_abs':np.nan,'p99_abs':np.nan,'nis':np.nan,
'chi_square_per_dof':np.nan}
nis = float(np.dot(a,a))
dof = int(a.size if effective_dof is None else effective_dof)
return {'count':int(a.size),'dof':dof,
'rms':float(np.sqrt(np.mean(a*a))),
'p50_abs':float(np.percentile(np.abs(a),50.)),
'p95_abs':float(np.percentile(np.abs(a),95.)),
'p99_abs':float(np.percentile(np.abs(a),99.)),
'nis':nis,'chi_square_per_dof':nis/max(dof,1)}
def _hpr_sigma(problem, node):
old = float(node.hpr_angular_sigma_rad)
extra_var = max(old*old-HPR_DIRECT_ANGULAR_SIGMA_RAD**2, 0.)
return float(np.sqrt(problem.hpr_direct_angular_sigma_rad**2+extra_var))
def _distribution(values):
a = np.asarray(values,dtype=float).reshape(-1)
if not a.size:
return {'count':0,'rms':np.nan,'p50':np.nan,'p95':np.nan,'p99':np.nan}
return {'count':len(a),'rms':float(np.sqrt(np.mean(a*a))),
'p50':float(np.percentile(a,50.)),'p95':float(np.percentile(a,95.)),
'p99':float(np.percentile(a,99.))}
def _vector_stats(values):
a = np.asarray(values,dtype=float).reshape(-1,3)
if not a.size:
return {'count':0,'axis_rms':[np.nan]*3,'axis_p95_abs':[np.nan]*3,
'vector_rms':np.nan,'vector_p95':np.nan}
norm = np.linalg.norm(a,axis=1)
return {'count':len(a),'axis_rms':np.sqrt(np.mean(a*a,axis=0)),
'axis_p95_abs':np.percentile(np.abs(a),95.,axis=0),
'vector_rms':float(np.sqrt(np.mean(norm*norm))),
'vector_p95':float(np.percentile(norm,95.))}
def residual(problem,x,details=None,dependencies=None,lever_override=None):
nodes, preints = problem.segment.nodes, problem.segment.preintegrations
lever = (problem.fixed_l_I_m if lever_override is None
else np.asarray(lever_override,dtype=float))
baseline_I = problem.R_RTK_IMU.T[:,0]
values = []
def add(value,label,node_indices,raw=None):
a = np.asarray(value,dtype=float).reshape(-1)
values.extend(a)
if dependencies is not None:
dependencies.extend([tuple(node_indices)]*len(a))
if details is not None:
details.setdefault(label,[]).extend(a.tolist())
if raw is not None: details.setdefault(label+'_physical',[]).append(np.asarray(raw))
for index,node in enumerate(nodes):
offset = NODE_DOF*index
R = _rotation(problem,index,x)
p, v = x[offset+3:offset+6], x[offset+6:offset+9]
bg, ba = x[offset+9:offset+12], x[offset+12:offset+15]
p_error = p+R@lever-node.p_enu_m
if node.source == 'GGA':
add(p_error[:2]/.06,'gga_xy',(index,))
if details is not None: details.setdefault('position_physical',[]).append(
np.array([p_error[0],p_error[1],np.nan]))
else:
add(p_error/np.array([.06,.06,.12]),'best_position',(index,),p_error)
if details is not None: details.setdefault('position_physical',[]).append(p_error)
if node.velocity_enu_m_s is not None:
v_error = v+R@np.cross(node.gyro_rad_s-bg,lever)-node.velocity_enu_m_s
add(v_error/np.array([.15,.15,.30]),'doppler',(index,),v_error)
if details is not None: details.setdefault('velocity_physical',[]).append(v_error)
if node.hpr_factor_valid:
hpr_error=np.cross(R@baseline_I,node.baseline_enu)
add(hpr_error/_hpr_sigma(problem,node),'hpr',(index,),hpr_error)
if node.gravity_candidate:
gravity = node.accel_m_s2-ba-R.T@(-G_ENU)
add(gravity/.12,'gravity',(index,))
if index == len(nodes)-1: continue
right = index+1
right_offset = NODE_DOF*right
Rj = _rotation(problem,right,x)
pj = x[right_offset+3:right_offset+6]
vj = x[right_offset+6:right_offset+9]
bgj = x[right_offset+9:right_offset+12]
baj = x[right_offset+12:right_offset+15]
pre = preints[index]
dR,dv,dp = apply_bias_correction_imu(pre,bg,ba)
dt = pre.duration_s
imu_error = np.concatenate([
so3_log(dR.T@R.T@Rj),
R.T@(vj-v-G_ENU*dt)-dv,
R.T@(pj-p-v*dt-.5*G_ENU*dt*dt)-dp])
add(residual_whiten_matrix(pre.cov)@imu_error,'imu_preintegration',
(index,right),imu_error)
add((bgj-bg)/(SIGMA_BG_RW*np.sqrt(dt)),'gyro_bias_random_walk',(index,right))
add((baj-ba)/(SIGMA_BA_RW*np.sqrt(dt)),'accel_bias_random_walk',(index,right))
add(x[:3]/np.deg2rad(5.),'initial_attitude_gauge',(0,))
add(x[9:12]/.02,'initial_gyro_bias',(0,))
add(x[12:15]/.5,'initial_accel_bias',(0,))
return np.asarray(values)
def jacobian_sparsity(problem,x):
dependencies = []
base = residual(problem,x,dependencies=dependencies)
sparsity = lil_matrix((len(base),len(x)),dtype=int)
for row,node_indices in enumerate(dependencies):
for index in node_indices:
start = NODE_DOF*index
sparsity[row,start:start+NODE_DOF] = 1
return sparsity.tocsr()
def _factor_stats(details):
return {key:_stats(value,2*len(value)//3 if key=='hpr' else None)
for key,value in details.items()
if not key.endswith('_physical') and key not in ('position_physical','velocity_physical')}
def _effective_residual_dimension(details):
return sum(2*len(v)//3 if k=='hpr' else len(v)
for k,v in details.items() if not k.endswith('_physical')
and k not in ('position_physical','velocity_physical'))
def _preintegration_covariance_stats(problem):
blocks = {'rotation_rad':[],'velocity_m_s':[],'position_m':[]}
for pre in problem.segment.preintegrations:
sigma = np.sqrt(np.maximum(np.diag(pre.cov),0.))
blocks['rotation_rad'].extend(sigma[:3])
blocks['velocity_m_s'].extend(sigma[3:6])
blocks['position_m'].extend(sigma[6:9])
return {key:_distribution(value) for key,value in blocks.items()}
def build_problem(segment,R_RTK_IMU,fixed_l_I_m,
hpr_direct_angular_sigma_rad=HPR_DIRECT_ANGULAR_SIGMA_RAD):
seeds = []
for index,node in enumerate(segment.nodes):
if node.hpr_factor_valid:
seeds.append(_world_rtk(node.baseline_enu)@R_RTK_IMU)
elif index:
seeds.append(seeds[-1]@segment.preintegrations[index-1].delta_R)
else:
seeds.append(segment.R_WRTK_initial@R_RTK_IMU)
return NodeGraphProblem(segment,tuple(seeds),np.asarray(fixed_l_I_m,dtype=float),
np.asarray(R_RTK_IMU,dtype=float),
float(hpr_direct_angular_sigma_rad))
def solve_fixed_lever(problem,max_nfev=30):
x0 = initial_parameters(problem)
initial_detail = {}
r0 = residual(problem,x0,initial_detail)
fit = least_squares(
lambda value:residual(problem,value),x0,jac='2-point',
jac_sparsity=jacobian_sparsity(problem,x0),method='trf',
tr_solver='lsmr',loss='linear',max_nfev=max_nfev,
x_scale='jac',ftol=1e-6,xtol=1e-6,gtol=1e-6)
final_detail = {}
rf = residual(problem,fit.x,final_detail)
statistical_dof = max(_effective_residual_dimension(final_detail)-len(fit.x),1)
bg = fit.x.reshape(-1,NODE_DOF)[:,9:12]
ba = fit.x.reshape(-1,NODE_DOF)[:,12:15]
bg_step = np.diff(bg,axis=0)
ba_step = np.diff(ba,axis=0)
return NodeGraphResult(
success=bool(fit.success),message=str(fit.message),
node_count=len(problem.segment.nodes),
duration_s=problem.segment.nodes[-1].t_s-problem.segment.nodes[0].t_s,
fixed_l_I_m=problem.fixed_l_I_m.copy(),
initial_cost=.5*float(np.dot(r0,r0)),final_cost=.5*float(np.dot(rf,rf)),
cost_reduction=.5*float(np.dot(r0,r0)-np.dot(rf,rf)),
nfev=int(fit.nfev),optimality=float(fit.optimality),
gradient_norm=float(np.linalg.norm(fit.grad)),
residual_dimension=len(rf),state_dimension=len(fit.x),
statistical_dof=statistical_dof,total_nis=float(np.dot(rf,rf)),
chi_square_per_dof=float(np.dot(rf,rf)/statistical_dof),
cost_per_dof=.5*float(np.dot(rf,rf)/statistical_dof),
initial_residual_by_factor=_factor_stats(initial_detail),
final_residual_by_factor=_factor_stats(final_detail),
final_position_residual_m=_vector_stats(final_detail.get('position_physical',[])),
final_velocity_residual_m_s=_vector_stats(final_detail.get('velocity_physical',[])),
max_bg_step_rad_s=float(np.max(np.linalg.norm(bg_step,axis=1))) if len(bg_step) else 0.,
max_ba_step_m_s2=float(np.max(np.linalg.norm(ba_step,axis=1))) if len(ba_step) else 0.,
preintegration_covariance_sigma=_preintegration_covariance_stats(problem))
def fit_states_at_fixed_lever(problem,l_I_m,max_nfev=50,initial_state_values=None):
lever=np.asarray(l_I_m,dtype=float)
x0=(initial_parameters(problem,lever) if initial_state_values is None
else np.asarray(initial_state_values,dtype=float))
r0=residual(problem,x0,lever_override=lever)
fit=least_squares(
lambda value:residual(problem,value,lever_override=lever),x0,jac='2-point',
jac_sparsity=jacobian_sparsity(problem,x0),method='trf',tr_solver='lsmr',
loss='linear',max_nfev=max_nfev,x_scale='jac',
ftol=1e-6,xtol=1e-6,gtol=1e-6)
return fit.x,{'success':bool(fit.success),'message':str(fit.message),
'nfev':int(fit.nfev),'initial_cost':.5*float(r0@r0),
'cost':float(fit.cost)}
def summarize_fixed_state_values(problems,state_values,l_I_m):
details={}; residuals=[]
for problem,value in zip(problems,state_values):
local={}
residuals.append(residual(problem,np.asarray(value),details=local,
lever_override=l_I_m))
for key,items in local.items(): details.setdefault(key,[]).extend(items)
joined=np.concatenate(residuals)
state_dimension=sum(len(value) for value in state_values)
dof=max(_effective_residual_dimension(details)-state_dimension,1)
return {'cost':.5*float(np.dot(joined,joined)),
'total_nis':float(np.dot(joined,joined)),
'chi_square_per_dof':float(np.dot(joined,joined)/dof),
'statistical_dof':dof,'residual_by_factor':_factor_stats(details),
'best_position_physical_m':_vector_stats(details.get('best_position_physical',[])),
'doppler_physical_m_s':_vector_stats(details.get('doppler_physical',[])),
'hpr_physical_rad':_vector_stats(details.get('hpr_physical',[]))}
def solve_fixed_lever_many(problems,max_nfev=30):
problems = tuple(problems)
sizes = [NODE_DOF*len(problem.segment.nodes) for problem in problems]
offsets = np.cumsum([0,*sizes])
x0 = np.concatenate([initial_parameters(problem) for problem in problems])
def evaluate(value,details=None):
chunks = []
for index,problem in enumerate(problems):
local_details = {} if details is not None else None
chunks.append(residual(problem,value[offsets[index]:offsets[index+1]],
local_details))
if details is not None:
for key,items in local_details.items():
details.setdefault(key,[]).extend(items)
return np.concatenate(chunks)
initial_detail = {}
r0 = evaluate(x0,initial_detail)
sparsity = lil_matrix((len(r0),len(x0)),dtype=int)
row = 0
for index,problem in enumerate(problems):
local_x = x0[offsets[index]:offsets[index+1]]
local = jacobian_sparsity(problem,local_x)
sparsity[row:row+local.shape[0],offsets[index]:offsets[index+1]] = local
row += local.shape[0]
fit = least_squares(
lambda value:evaluate(value),x0,jac='2-point',jac_sparsity=sparsity.tocsr(),
method='trf',tr_solver='lsmr',loss='linear',max_nfev=max_nfev,
x_scale='jac',ftol=1e-6,xtol=1e-6,gtol=1e-6)
final_detail = {}
rf = evaluate(fit.x,final_detail)
bg_steps, ba_steps = [], []
for index,problem in enumerate(problems):
states = fit.x[offsets[index]:offsets[index+1]].reshape(-1,NODE_DOF)
bg_steps.extend(np.linalg.norm(np.diff(states[:,9:12],axis=0),axis=1))
ba_steps.extend(np.linalg.norm(np.diff(states[:,12:15],axis=0),axis=1))
covariance = {'rotation_rad':[],'velocity_m_s':[],'position_m':[]}
for problem in problems:
for pre in problem.segment.preintegrations:
sigma = np.sqrt(np.maximum(np.diag(pre.cov),0.))
covariance['rotation_rad'].extend(sigma[:3])
covariance['velocity_m_s'].extend(sigma[3:6])
covariance['position_m'].extend(sigma[6:9])
dof = max(_effective_residual_dimension(final_detail)-len(fit.x),1)
return NodeGraphResult(
success=bool(fit.success),message=str(fit.message),
node_count=sum(len(problem.segment.nodes) for problem in problems),
duration_s=sum(problem.segment.nodes[-1].t_s-problem.segment.nodes[0].t_s
for problem in problems),
fixed_l_I_m=problems[0].fixed_l_I_m.copy(),
initial_cost=.5*float(np.dot(r0,r0)),final_cost=.5*float(np.dot(rf,rf)),
cost_reduction=.5*float(np.dot(r0,r0)-np.dot(rf,rf)),
nfev=int(fit.nfev),optimality=float(fit.optimality),
gradient_norm=float(np.linalg.norm(fit.grad)),
residual_dimension=len(rf),state_dimension=len(fit.x),
statistical_dof=dof,total_nis=float(np.dot(rf,rf)),
chi_square_per_dof=float(np.dot(rf,rf)/dof),
cost_per_dof=.5*float(np.dot(rf,rf)/dof),
initial_residual_by_factor=_factor_stats(initial_detail),
final_residual_by_factor=_factor_stats(final_detail),
final_position_residual_m=_vector_stats(final_detail.get('position_physical',[])),
final_velocity_residual_m_s=_vector_stats(final_detail.get('velocity_physical',[])),
max_bg_step_rad_s=float(max(bg_steps,default=0.)),
max_ba_step_m_s2=float(max(ba_steps,default=0.)),
preintegration_covariance_sigma={key:_distribution(value) for key,value in covariance.items()})
def _free_residual(problem,value,details=None):
return residual(problem,value[3:],details=details,lever_override=value[:3])
def _free_sparsity(problem,value):
local = jacobian_sparsity(problem,value[3:])
result = lil_matrix((local.shape[0],local.shape[1]+3),dtype=int)
result[:,:3] = 1
result[:,3:] = local
return result.tocsr()
def _marginal_lever_information(jacobian):
J = jacobian.toarray() if hasattr(jacobian,'toarray') else np.asarray(jacobian)
H = J.T@J
Hll,Hln,Hnn = H[:3,:3],H[:3,3:],H[3:,3:]
marginal = Hll-Hln@np.linalg.pinv(Hnn,rcond=1e-10)@Hln.T
return .5*(marginal+marginal.T)
def _additive_marginal_lever_information(jacobian,row_offsets,state_offsets):
total=np.zeros((3,3))
for index in range(len(row_offsets)-1):
rows=slice(row_offsets[index],row_offsets[index+1])
columns=np.r_[0:3,state_offsets[index]:state_offsets[index+1]]
local=jacobian[rows,:][:,columns]
total+=_marginal_lever_information(local)
return .5*(total+total.T)
def linearized_lever_information(problem,l_I_m):
lever=np.asarray(l_I_m,dtype=float)
value=np.concatenate([lever,initial_parameters(problem,lever)])
fit=least_squares(lambda x:_free_residual(problem,x),value,jac='2-point',
jac_sparsity=_free_sparsity(problem,value),method='trf',tr_solver='lsmr',
loss='linear',max_nfev=1,x_scale='jac')
information=_marginal_lever_information(fit.jac)
_,singular,Vt=np.linalg.svd(information)
covariance=np.linalg.pinv(information,rcond=1e-9)
return information,covariance,singular,Vt[-1]
def solve_free_lever(problem,initial_l_I_m,max_nfev=120):
initial_l = np.asarray(initial_l_I_m,dtype=float)
x0 = np.concatenate([initial_l,initial_parameters(problem,initial_l)])
r0 = _free_residual(problem,x0)
fit = least_squares(
lambda value:_free_residual(problem,value),x0,jac='2-point',
jac_sparsity=_free_sparsity(problem,x0),method='trf',tr_solver='lsmr',
loss='linear',max_nfev=max_nfev,x_scale='jac',
ftol=1e-6,xtol=1e-6,gtol=1e-6)
detail = {}
rf = _free_residual(problem,fit.x,detail)
information = _marginal_lever_information(fit.jac)
_,singular_values,Vt = np.linalg.svd(information)
tolerance = max(singular_values[0]*1e-9,1e-10)
rank = int(np.sum(singular_values>tolerance))
covariance = np.linalg.pinv(information,rcond=1e-9)
dof = max(_effective_residual_dimension(detail)-len(fit.x),1)
condition = (float(singular_values[0]/singular_values[-1])
if singular_values[-1]>tolerance else np.inf)
return FreeLeverResult(
success=bool(fit.success),message=str(fit.message),
initial_l_I_m=initial_l,final_l_I_m=fit.x[:3].copy(),
lever_step_norm_m=float(np.linalg.norm(fit.x[:3]-initial_l)),
initial_cost=.5*float(np.dot(r0,r0)),
final_cost=.5*float(np.dot(rf,rf)),nfev=int(fit.nfev),
optimality=float(fit.optimality),chi_square_per_dof=float(np.dot(rf,rf)/dof),
position_residual_m=_vector_stats(detail.get('position_physical',[])),
velocity_residual_m_s=_vector_stats(detail.get('velocity_physical',[])),
residual_by_factor=_factor_stats(detail),lever_covariance_m2=covariance,
lever_information_singular_values=singular_values,
lever_information_condition_number=condition,lever_precision_rank=rank,
weakest_lever_direction_I=Vt[-1].copy())
def solve_free_lever_many(problems,initial_l_I_m,max_nfev=120,
initial_state_values=None,lever_prior_mean_m=None,
lever_prior_covariance_m2=None,return_state_values=False):
problems=tuple(problems)
initial_l=np.asarray(initial_l_I_m,dtype=float)
sizes=[NODE_DOF*len(problem.segment.nodes) for problem in problems]
offsets=np.cumsum([3,*sizes])
states=([initial_parameters(problem,initial_l) for problem in problems]
if initial_state_values is None else
[np.asarray(value,dtype=float) for value in initial_state_values])
prior_mean=(None if lever_prior_mean_m is None else
np.asarray(lever_prior_mean_m,dtype=float))
prior_cov=(None if lever_prior_covariance_m2 is None else
np.asarray(lever_prior_covariance_m2,dtype=float))
prior_whitener=(None if prior_cov is None else
np.linalg.inv(np.linalg.cholesky(prior_cov)))
x0=np.concatenate([initial_l,*states])
def evaluate(value,details=None):
chunks=[]
for index,problem in enumerate(problems):
local={} if details is not None else None
chunks.append(residual(problem,value[offsets[index]:offsets[index+1]],
details=local,lever_override=value[:3]))
if details is not None:
for key,items in local.items(): details.setdefault(key,[]).extend(items)
if prior_whitener is not None:
prior_error=prior_whitener@(value[:3]-prior_mean)
chunks.append(prior_error)
if details is not None:
details.setdefault('lever_prior',[]).extend(prior_error.tolist())
return np.concatenate(chunks)
r0=evaluate(x0)
sparsity=lil_matrix((len(r0),len(x0)),dtype=int)
row=0; row_offsets=[0]
for index,problem in enumerate(problems):
local=jacobian_sparsity(problem,states[index])
sparsity[row:row+local.shape[0],:3]=1
sparsity[row:row+local.shape[0],offsets[index]:offsets[index+1]]=local
row+=local.shape[0]
row_offsets.append(row)
if prior_whitener is not None:
sparsity[row:row+3,:3]=1
fit=least_squares(
lambda value:evaluate(value),x0,jac='2-point',jac_sparsity=sparsity.tocsr(),
method='trf',tr_solver='lsmr',loss='linear',max_nfev=max_nfev,
x_scale='jac',ftol=1e-6,xtol=1e-6,gtol=1e-6)
detail={}
rf=evaluate(fit.x,detail)
information=_additive_marginal_lever_information(
fit.jac,row_offsets,offsets)
if prior_cov is not None:
information+=np.linalg.inv(prior_cov)
_,singular_values,Vt=np.linalg.svd(information)
tolerance=max(singular_values[0]*1e-9,1e-10)
rank=int(np.sum(singular_values>tolerance))
covariance=np.linalg.pinv(information,rcond=1e-9)
dof=max(_effective_residual_dimension(detail)-len(fit.x),1)
condition=(float(singular_values[0]/singular_values[-1])
if singular_values[-1]>tolerance else np.inf)
result=FreeLeverResult(
success=bool(fit.success),message=str(fit.message),
initial_l_I_m=initial_l,final_l_I_m=fit.x[:3].copy(),
lever_step_norm_m=float(np.linalg.norm(fit.x[:3]-initial_l)),
initial_cost=.5*float(np.dot(r0,r0)),final_cost=.5*float(np.dot(rf,rf)),
nfev=int(fit.nfev),optimality=float(fit.optimality),
chi_square_per_dof=float(np.dot(rf,rf)/dof),
position_residual_m=_vector_stats(detail.get('position_physical',[])),
velocity_residual_m_s=_vector_stats(detail.get('velocity_physical',[])),
residual_by_factor=_factor_stats(detail),lever_covariance_m2=covariance,
lever_information_singular_values=singular_values,
lever_information_condition_number=condition,lever_precision_rank=rank,
weakest_lever_direction_I=Vt[-1].copy())
if return_state_values:
states=[fit.x[offsets[i]:offsets[i+1]].copy()
for i in range(len(problems))]
return result,states
return result