feat(ccd): publish writing-ready corrected analysis
Consolidate the validated Karman CCD chain into a bounded, navigable package with reproducible diagnostics and lightweight plotting contracts while keeping dense evidence external. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import numpy as np
|
||||
import pytest
|
||||
from CCD_analysis.karman_dynamic.mode_diagnostics import (action_coordinates,bounded_streamwise_shift_correlation,component_major_velocity,masked_vorticity,phase_harmonics,reshape_cycle_phase,subspace_symmetry_diagnostics,symmetry_diagnostics,x_localization)
|
||||
from CCD_analysis.karman_dynamic.pod_baseline import align_basis_to_reference,fit_weighted_pod,weighted_principal_cosines
|
||||
|
||||
|
||||
def test_component_major_vorticity_sign_and_xy_orientation():
|
||||
x=np.array([-1.,-.4,.2,1.,2.1]); y=np.array([-2.,-.8,0.,.9,2.]); xx,yy=np.meshgrid(x,y,indexing="ij"); mask=np.ones(xx.shape,bool)
|
||||
velocity=component_major_velocity(np.r_[(-yy)[mask],xx[mask]],mask); omega,valid=masked_vorticity(*velocity,x,y,mask)
|
||||
assert np.all(valid); np.testing.assert_allclose(omega,2.,rtol=1e-13,atol=1e-13)
|
||||
with pytest.raises(ValueError,match="coordinate"): masked_vorticity(*velocity,y[:-1],x,mask)
|
||||
|
||||
|
||||
def test_masked_vorticity_requires_complete_three_point_stencils():
|
||||
x=np.array([0.,.2,.7,1.5,2.6]); y=np.array([-1.,-.2,.5,1.4,2.5]); xx,yy=np.meshgrid(x,y,indexing="ij"); mask=np.ones(xx.shape,bool); mask[2,2]=False
|
||||
omega,valid=masked_vorticity(np.zeros_like(xx),xx**2,x,y,mask)
|
||||
assert np.isfinite(omega).all() and not valid[1:4,2].any() and not valid[2,1:4].any(); np.testing.assert_allclose(omega[valid],2*xx[valid],rtol=1e-13,atol=1e-13)
|
||||
|
||||
|
||||
def test_vector_parity_and_subspace_invariance():
|
||||
x=np.linspace(0,4,9); y=np.linspace(-2,2,7); xx,yy=np.meshgrid(x,y,indexing="ij"); mask=np.ones(xx.shape,bool); weights=np.ones_like(xx)
|
||||
velocity=np.stack((np.exp(-(xx-2)**2),np.zeros_like(xx))); diagnostics=symmetry_diagnostics(velocity,weights,mask,y)
|
||||
assert diagnostics["symmetric_fraction"]==pytest.approx(1) and diagnostics["antisymmetric_fraction"]==pytest.approx(0)
|
||||
columns=np.column_stack((np.r_[np.exp(-(xx-1.5)**2)[mask],(yy*np.exp(-(xx-2.5)**2))[mask]],np.r_[(yy*np.exp(-(xx-1.5)**2))[mask],np.exp(-(xx-2.5)**2)[mask]]))
|
||||
component_weights=np.tile(weights[mask],2); q,_=np.linalg.qr(np.sqrt(component_weights)[:,None]*columns); modes=q/np.sqrt(component_weights)[:,None]
|
||||
subspace=subspace_symmetry_diagnostics(modes,weights,mask,y); np.testing.assert_allclose(subspace["reflection_principal_cosines"],1); assert subspace["invariance_fraction"]==pytest.approx(1)
|
||||
rotation=np.array([[1.,1.],[-1.,1.]])/np.sqrt(2); rotated=subspace_symmetry_diagnostics(modes@rotation,weights,mask,y)
|
||||
assert rotated["symmetric_fraction"]==pytest.approx(subspace["symmetric_fraction"]); assert x_localization(np.exp(-8*(xx-2)**2),x,weights,mask)["centroid_x_D"]==pytest.approx(2)
|
||||
|
||||
|
||||
def test_harmonics_require_19_by_10_and_retain_each_cycle():
|
||||
phase=2*np.pi*np.arange(10)/10; drift=np.arange(19)[:,None]*.02; signal=3*np.cos(phase[None,:]-.4+drift)+2*np.cos(2*phase[None,:]+.2)
|
||||
harmonics=phase_harmonics(reshape_cycle_phase(signal.reshape(-1)),2)
|
||||
assert harmonics["per_cycle_amplitude"].shape==(19,3) and harmonics["mean_cycle_amplitude"][1]==pytest.approx(3) and harmonics["coherent_amplitude"][1]<3 and harmonics["coherent_amplitude"][2]==pytest.approx(2)
|
||||
with pytest.raises(ValueError,match="explicit"): phase_harmonics(np.zeros(190))
|
||||
with pytest.raises(ValueError): reshape_cycle_phase(np.zeros(189))
|
||||
|
||||
|
||||
def test_action_transform_literal_definitions_and_inverse():
|
||||
native=np.array([[2.,4.],[3.,5.],[1.,-1.]]); transformed=action_coordinates(native)
|
||||
np.testing.assert_allclose(transformed[0],native[0]); np.testing.assert_allclose(transformed[1],(native[1]+native[2])/np.sqrt(2)); np.testing.assert_allclose(transformed[2],(native[1]-native[2])/np.sqrt(2)); np.testing.assert_allclose(action_coordinates(transformed,inverse=True),native)
|
||||
|
||||
|
||||
def test_shift_sign_weights_and_finite_guard():
|
||||
x=np.arange(8.); a=np.zeros((2,8,3)); b=np.zeros_like(a); a[:,2]=np.array([[1.],[2.]]); b[:,4]=a[:,2]; weights=np.linspace(1,2,24).reshape(8,3); mask=np.ones((8,3),bool)
|
||||
result=bounded_streamwise_shift_correlation(a,b,x,weights,mask,3); assert result["best_shift_x_D"]==pytest.approx(2) and result["correlation"]==pytest.approx(1) and result["index_offset"]==2
|
||||
b[:,4,0]=np.nan; b[:,4,1:]=a[:,2,1:]; assert np.isfinite(bounded_streamwise_shift_correlation(a,b,x,weights,mask,3)["correlation"])
|
||||
|
||||
|
||||
def test_weighted_pod_residual_cosines_and_alignment():
|
||||
rng=np.random.default_rng(4); snapshots=rng.normal(size=(12,3))@rng.normal(size=(3,20)); weights=np.linspace(.5,2,12); pod=fit_weighted_pod(snapshots,weights,3)
|
||||
np.testing.assert_allclose(pod["modes"].T@(weights[:,None]*pod["modes"]),np.eye(3),atol=1e-12); assert np.all(np.diff(pod["weighted_residual_fraction"])<=0) and pod["weighted_residual_fraction"][-1]<1e-25; np.testing.assert_allclose(weighted_principal_cosines(pod["modes"],pod["modes"],weights),1)
|
||||
rotation,_=np.linalg.qr(rng.normal(size=(3,3))); aligned,recovered=align_basis_to_reference(pod["modes"],pod["modes"]@rotation,weights); np.testing.assert_allclose(aligned,pod["modes"],atol=1e-12); np.testing.assert_allclose(recovered.T@recovered,np.eye(3),atol=1e-12)
|
||||
with pytest.raises(ValueError,match="orthonormal"): weighted_principal_cosines(2*pod["modes"],pod["modes"],weights)
|
||||
Reference in New Issue
Block a user