"""Shared D/i Jacobian, coordinate-invariant likelihood and refusal checks.""" import math import copy import numpy as np from rotation_model import forward,observation_coordinate,log_likelihood def verify(record): rows=np.asarray(record['rows']);meta=record['metadata'];distance=meta['distanceMpc'];inclination=meta['inclinationDegrees'];radians=math.radians(inclination) gas=rows[:,3]*np.abs(rows[:,3]);disk=.5*rows[:,4]*np.abs(rows[:,4]) def coordinates(d_value,i_value): d=d_value/distance;k=math.sin(radians)/math.sin(math.radians(i_value)) return np.concatenate([rows[:,0]*d,rows[:,1]*k,rows[:,2]*k,gas*d,disk*d]) count=len(rows);zero=np.zeros(count) analytic_d=np.concatenate([rows[:,0]/distance,zero,zero,gas/distance,disk/distance]) cotangent_per_degree=math.cos(radians)/math.sin(radians)*math.pi/180 analytic_i=np.concatenate([zero,-rows[:,1]*cotangent_per_degree,-rows[:,2]*cotangent_per_degree,zero,zero]) measured_d=(coordinates(distance*(1+1e-5),inclination)-coordinates(distance*(1-1e-5),inclination))/(2e-5*distance) measured_i=(coordinates(distance,inclination+1e-5)-coordinates(distance,inclination-1e-5))/2e-5 errors=[float(np.max(np.abs(measured-analytic)/np.maximum(np.abs(analytic),1))) for measured,analytic in [(measured_d,analytic_d),(measured_i,analytic_i)]] projected,error=observation_coordinate(record);likelihood_tests=[] for angle in [54.,63.,72.]: prediction=forward(record,.55,1.4e7,9,'nfw',1.,angle) native=log_likelihood(record,prediction);k=math.sin(radians)/math.sin(math.radians(angle)) observed_scaled=rows[:,1]*k;sigma_scaled=rows[:,2]*k scaled=float(-.5*np.sum(((observed_scaled-prediction['circularVelocityKms'])/sigma_scaled)**2+np.log(2*math.pi*sigma_scaled*sigma_scaled))) restored_original=scaled+count*math.log(k) restored=restored_original-count*math.log(math.sin(radians)) likelihood_tests.append({'inclinationDegrees':angle,'referenceLogLikelihood':native,'scaledLogLikelihood':scaled,'restoredOriginalDeprojectedCoordinate':restored_original,'jacobianRestoredReferenceProjection':restored,'absoluteDifference':abs(native-restored),'gate':1e-8,'passed':abs(native-restored)<=1e-8}) shift=analytic_i[count:2*count]*meta['inclinationErrorDegrees'];covariance=np.outer(shift,shift) common_mean_variance=float(np.sum(covariance)/count**2);incorrect_independent_mean_variance=float(np.trace(covariance)/count**2) refusals=[] operations=[('nonfinite',lambda:forward(record,float('nan'),1e7,5,'nfw')),('negative-ML',lambda:forward(record,-1,1e7,5,'nfw')),('face-on',lambda:forward(record,.5,1e7,5,'nfw',1,5)),('zero-scale',lambda:forward(record,.5,1e7,0,'nfw'))] outward=copy.deepcopy(record);outward['rows'][0][3]=-1000;outward['rows'][0][4]=0 operations.append(('outward-total',lambda:forward(outward,0,1,1,'nfw'))) for label,operation in operations: try:operation();refusals.append({'case':label,'refused':False}) except ValueError:refusals.append({'case':label,'refused':True}) signed=forward(record,.5,1e7,5,'nfw');outward['rows'][0][3]=-1 negative=forward(outward,.5,1e7,5,'nfw') negative_correct=negative['gasSignedSpeedSquared'][0]==-1 return {'distanceInclinationJacobianErrors':errors,'jacobianGate':1e-7,'likelihoodCoordinateChecks':likelihood_tests, 'sharedInclinationMeanVariance':common_mean_variance,'incorrectIndependentMeanVariance':incorrect_independent_mean_variance, 'naiveIndependentSystematicAveragingFactor':common_mean_variance/incorrect_independent_mean_variance, 'perfectFitWrongNormalizationBiasBetween54And72':count*math.log(math.sin(math.radians(72))/math.sin(math.radians(54))), 'refusals':refusals,'negativeGasSignRetained':bool(negative_correct), 'allRequiredPassed':max(errors)<=1e-7 and all(row['passed'] for row in likelihood_tests) and all(row['refused'] for row in refusals) and bool(negative_correct), 'scope':'Conditional native-profile homology/global inclination only, not raw velocity-field reconstruction or a complete measurement covariance'}