"""Small standalone checks of the production colored CVODE Jacobian callback. The independent oracle is the actual SUNDIALS 7.4 cvLsDenseDQJac symbol in the installed static library for the unchanged cache path. Canonical-mode tests separately verify a recomputed baseline and the original-fy perturbation policy; that derivative is intentionally not equated to the old shared-cache DQ. Its private headers are used only by this test TU, never by production code. No full application/model compilation is required. """ from __future__ import annotations import os from pathlib import Path import subprocess import sys import tempfile import unittest from app.simulation.native_codegen.build import LIBRARIES, toolchain ROOT = Path(__file__).resolve().parents[1] SOURCE = ROOT / "test/solver-newton-20260911/toolchain/sundials-7.4.0" MODEL_HEADER = r''' #ifndef TEST_MODEL_H #define TEST_MODEL_H #define NSTATES 4 #define NOUTPUTS 4 #define MODEL_JACOBIAN_COLORED 1 #define MODEL_JACOBIAN_COLOR_COUNT 2 #define MODEL_JACOBIAN_NNZ 8 extern int model_jacobian_col_ptr[5], model_jacobian_row_index[8], model_jacobian_column_color[4]; extern const double model_atol[4]; double model_next_break(double time,double end); #endif ''' HARNESS = r''' #include #include #include #include #include "cvode_impl.h" #include "cvode_ls_impl.h" #include "@RUNTIME@" #if SUNDIALS_VERSION_MAJOR != 7 || SUNDIALS_VERSION_MINOR != 4 || SUNDIALS_VERSION_PATCH != 0 #error This private-ABI test must use SUNDIALS 7.4.0 #endif int model_jacobian_col_ptr[5]={0,2,4,6,8}; int model_jacobian_row_index[8]={0,1,0,1,2,3,2,3}; int model_jacobian_column_color[4]={0,1,0,1}; const double model_atol[4]={1e-9,1e-9,1e-9,1e-9}; static int assertions, zero_model, extra_dependency, reject_mode, poll_calls, cancel_poll; static int break_enabled, event_enabled, event_done; static double baseline_state[NSTATES]; static int canonical_trace, canonical_count, reject_canonical_base; static double canonical_inputs[16][NSTATES]; #define CHECK(condition) do { assertions++; if(!(condition)){fprintf(stderr,"check failed at line %d: %s\n",__LINE__,#condition);exit(1);} } while(0) static int evaluate(const double *y,double *f) { int perturbed=0; for(int i=0;i1) || (reject_mode==2 && perturbed>0))return 0; if(zero_model){for(int i=0;i=cancel_poll){r->status=1;return 0;} return 1; } int native_rhs(NativeRun *r,double time,const double *state,double *derivative) { (void)time;r->nfev++;return evaluate(state,derivative); } int native_jacobian_rhs(NativeRun *r,double time,const double *state,double *derivative) { (void)time;r->nfev++; if(canonical_trace) { CHECK(canonical_count<16); memcpy(canonical_inputs[canonical_count++],state,NSTATES*sizeof(double)); } if(reject_canonical_base && !memcmp(state,baseline_state,sizeof(baseline_state)))return 0; return evaluate(state,derivative); } int native_append(NativeRun *r,double time,const double *state) { r->final_time=time;memcpy(r->final_state,state,NSTATES*sizeof(double));return 1; } int native_accept(NativeRun *r,double time,double next,const double *old,const double *trial, NativeDense dense,void *context,double *accepted_time,double *accepted_state) { (void)time;(void)old;(void)dense;(void)context; *accepted_time=next;memcpy(accepted_state,trial,NSTATES*sizeof(double)); int impact=event_enabled && !event_done && next>=0.0005; if(impact){event_done=1;accepted_state[0]=-accepted_state[0];r->events++;} r->final_time=next;memcpy(r->final_state,accepted_state,NSTATES*sizeof(double));return impact; } double model_next_break(double time,double end) {return break_enabled && time<0.001 && end>0.001?0.001:end;} typedef struct { NativeRun run; CvContext context; SUNContext sun; SUNLinearSolver linear; N_Vector y,fy,trial,ftrial,tmp3,oracle_state; SUNMatrix matrix,oracle; } Fixture; static void fixture_create(Fixture *f) { memset(f,0,sizeof(*f));CHECK(!SUNContext_Create(SUN_COMM_NULL,&f->sun)); f->y=N_VNew_Serial(NSTATES,f->sun);f->fy=N_VClone(f->y);f->trial=N_VClone(f->y); f->ftrial=N_VClone(f->y);f->tmp3=N_VClone(f->y);f->oracle_state=N_VClone(f->y); f->matrix=SUNDenseMatrix(NSTATES,NSTATES,f->sun);f->oracle=SUNDenseMatrix(NSTATES,NSTATES,f->sun); f->linear=SUNLinSol_Dense(f->y,f->matrix,f->sun); f->context=(CvContext){.run=&f->run,.solver=CVodeCreate(CV_BDF,f->sun),.weights=N_VClone(f->y),.colored=1}; CHECK(f->y && f->fy && f->trial && f->ftrial && f->tmp3 && f->oracle_state && f->matrix && f->oracle && f->linear && f->context.solver && f->context.weights); N_VConst(1,f->y); CHECK(!CVodeInit(f->context.solver,cv_rhs,0,f->y)); CHECK(!CVodeSetUserData(f->context.solver,&f->context)); CHECK(!CVodeSStolerances(f->context.solver,1e-8,1e-9)); CHECK(!CVodeSetLinearSolver(f->context.solver,f->linear,f->matrix)); f->run.jacobian_colored=1; } static void fixture_set(Fixture *f,const double *state,const double *weights,double step) { memcpy(N_VGetArrayPointer(f->y),state,NSTATES*sizeof(double)); memcpy(baseline_state,state,sizeof(baseline_state));CHECK(evaluate(state,N_VGetArrayPointer(f->fy))); /* Set the actual library's trial-step state, not a second formula oracle. */ CVodeMem memory=(CVodeMem)f->context.solver; memory->cv_h=step;memory->cv_next_h=step; memcpy(N_VGetArrayPointer(memory->cv_ewt),weights,NSTATES*sizeof(double)); } static void fixture_free(Fixture *f) { CVodeFree(&f->context.solver);SUNLinSolFree(f->linear); if(f->context.reference)SUNMatDestroy(f->context.reference); SUNMatDestroy(f->matrix);SUNMatDestroy(f->oracle);N_VDestroy(f->context.weights); N_VDestroy(f->y);N_VDestroy(f->fy);N_VDestroy(f->trial);N_VDestroy(f->ftrial);N_VDestroy(f->tmp3);N_VDestroy(f->oracle_state); SUNContext_Free(&f->sun); } static int callback(Fixture *f) { return cv_jacobian(0,f->y,f->fy,f->matrix,&f->context,f->ftrial,f->trial,f->tmp3); } static void actual_upstream_oracle(Fixture *f) { memcpy(N_VGetArrayPointer(f->oracle_state),N_VGetArrayPointer(f->y),NSTATES*sizeof(double)); CHECK(!cvLsDenseDQJac(0,f->oracle_state,f->fy,f->oracle,(CVodeMem)f->context.solver,f->ftrial)); CHECK(!memcmp(N_VGetArrayPointer(f->oracle_state),N_VGetArrayPointer(f->y),NSTATES*sizeof(double))); } static void matrices_equal(Fixture *f) { for(int j=0;jmatrix,i,j),b=SM_ELEMENT_D(f->oracle,i,j); if(memcmp(&a,&b,sizeof(double)))fprintf(stderr,"matrix mismatch (%d,%d): %.17g vs %.17g\n",i,j,a,b); CHECK(!memcmp(&a,&b,sizeof(double))); } } static void formula_oracle(void) { Fixture f;fixture_create(&f); const double states[][4]={{0,1e-8,-2,1e4},{.1,-.3,1.25,-4.25},{-1e-20,0,2e-10,3}}; const double weights[][4]={{1e12,1e8,1e-4,1e-10},{2,.5,4,1},{1e-3,1e10,1e4,1}}; const double steps[]={1e-12,1e-3,-3e-4,2e5}; for(int kind=0;kind<2;kind++)for(int state=0;state<3;state++)for(int step=0;step<4;step++) { zero_model=kind;fixture_set(&f,states[state],weights[state],steps[step]); double saved_y[NSTATES],saved_f[NSTATES]; memcpy(saved_y,N_VGetArrayPointer(f.y),sizeof(saved_y));memcpy(saved_f,N_VGetArrayPointer(f.fy),sizeof(saved_f)); unsigned long before=f.run.jacobian_rhs; CHECK(!callback(&f));CHECK(f.run.jacobian_rhs-before==MODEL_JACOBIAN_COLOR_COUNT); CHECK(!memcmp(saved_y,N_VGetArrayPointer(f.y),sizeof(saved_y)));CHECK(!memcmp(saved_f,N_VGetArrayPointer(f.fy),sizeof(saved_f))); actual_upstream_oracle(&f);matrices_equal(&f); } zero_model=0; const double round_state[]={.1,.1,.1,.1},round_weights[]={1e6,1e6,1e6,1e6};double inc[NSTATES]; fixture_set(&f,round_state,round_weights,1e-12);CHECK(!jac_increments(&f.context,f.y,f.fy,inc)); CHECK((round_state[0]+inc[0])-round_state[0]!=inc[0]); CHECK(!callback(&f));actual_upstream_oracle(&f);matrices_equal(&f); fixture_free(&f); } static void coloring_validation(void) { CHECK(valid_coloring()); model_jacobian_col_ptr[0]=1;CHECK(!valid_coloring());model_jacobian_col_ptr[0]=0; model_jacobian_col_ptr[2]=9;CHECK(!valid_coloring());model_jacobian_col_ptr[2]=4; model_jacobian_row_index[1]=0;CHECK(!valid_coloring());model_jacobian_row_index[1]=1; model_jacobian_row_index[1]=NSTATES;CHECK(!valid_coloring());model_jacobian_row_index[1]=1; model_jacobian_row_index[0]=-1;CHECK(!valid_coloring());model_jacobian_row_index[0]=0; model_jacobian_column_color[1]=0;CHECK(!valid_coloring());model_jacobian_column_color[1]=1; model_jacobian_column_color[1]=2;CHECK(!valid_coloring());model_jacobian_column_color[1]=1; CHECK(valid_coloring()); } static void recoverable_failure(int mode) { Fixture f;fixture_create(&f);const double state[]={1.2,2.1,3.3,4.4},weight[]={1,1,1,1}; fixture_set(&f,state,weight,.001);double saved_f[NSTATES];memcpy(saved_f,N_VGetArrayPointer(f.fy),sizeof(saved_f)); reject_mode=mode;int code=callback(&f);CHECK(code==(mode==1?0:1)); CHECK(f.run.jacobian_fallbacks==1);CHECK(f.run.jacobian_colored_evals==0); CHECK(f.run.jacobian_rhs==(unsigned long)(mode==1?1+NSTATES:2));CHECK(f.run.nfev==f.run.jacobian_rhs); CHECK(!memcmp(state,N_VGetArrayPointer(f.y),sizeof(state)));CHECK(!memcmp(saved_f,N_VGetArrayPointer(f.fy),sizeof(saved_f))); CHECK(!memcmp(state,N_VGetArrayPointer(f.trial),sizeof(state))); reject_mode=0;if(mode==1){actual_upstream_oracle(&f);matrices_equal(&f);}fixture_free(&f); } static void cancellation(void) { for(int after=1;after<=2;after++) { Fixture f;fixture_create(&f);const double state[]={1.2,2.1,3.3,4.4},weight[]={1,1,1,1}; fixture_set(&f,state,weight,.001);double saved_f[NSTATES];memcpy(saved_f,N_VGetArrayPointer(f.fy),sizeof(saved_f)); cancel_poll=after;poll_calls=0;CHECK(callback(&f)<0); CHECK(f.run.jacobian_fallbacks==0);CHECK(f.run.jacobian_rhs==(unsigned long)(after-1));CHECK(f.run.nfev==f.run.jacobian_rhs); CHECK(f.run.status==1);CHECK(!memcmp(state,N_VGetArrayPointer(f.y),sizeof(state)));CHECK(!memcmp(saved_f,N_VGetArrayPointer(f.fy),sizeof(saved_f))); cancel_poll=0;fixture_free(&f); } } static void verify_missing_dependency(void) { Fixture f;fixture_create(&f);const double state[]={1.2,2.1,3.3,4.4},weight[]={1,1,1,1}; extra_dependency=1;fixture_set(&f,state,weight,.001); f.context.reference=SUNDenseMatrix(NSTATES,NSTATES,f.sun);CHECK(f.context.reference!=NULL);CHECK(valid_coloring()); CHECK(!callback(&f));CHECK(f.run.jacobian_rhs==MODEL_JACOBIAN_COLOR_COUNT+NSTATES); CHECK(f.run.jacobian_checks==1);CHECK(f.run.jacobian_mismatches==1);CHECK(f.run.jacobian_fallbacks==1); CHECK(!f.context.colored && !f.run.jacobian_colored);actual_upstream_oracle(&f);matrices_equal(&f); CHECK(!CVodeReInit(f.context.solver,.1,f.y)); const double fresh_weights[]={7,11,13,17};fixture_set(&f,state,fresh_weights,1e-5); unsigned long before=f.run.jacobian_rhs; CHECK(!callback(&f));CHECK(f.run.jacobian_rhs-before==NSTATES); CHECK(f.run.jacobian_checks==1 && f.run.jacobian_mismatches==1 && f.run.jacobian_fallbacks==1); CHECK(!f.context.colored);actual_upstream_oracle(&f);matrices_equal(&f); fixture_free(&f); } static NativeRun integration_run(int verify) { NativeRun run={0};run.jacobian_verify=verify; run.options=(NativeOptions){0,.002,.0001,.0002,1e-8,10,1,0,NULL}; for(int i=0;i0 && run.nlu>0 && run.accepted>0); return run; } static void integration_restart_counters(void) { break_enabled=1;event_enabled=1; /* No user mode selects default DQ. Invalid structural metadata must still fall back safely, including when diagnostic verification is requested. */ model_jacobian_column_color[1]=0;CHECK(!valid_coloring()); NativeRun fallback=integration_run(0),fallback_verify=integration_run(1); model_jacobian_column_color[1]=1;CHECK(valid_coloring()); NativeRun colored=integration_run(0),verify=integration_run(1); CHECK(!fallback.jacobian_colored && !fallback_verify.jacobian_colored); CHECK(fallback.jacobian_rhs==0 && fallback.linear_rhs==fallback.njev*NSTATES); CHECK(fallback_verify.jacobian_rhs==0 && fallback_verify.linear_rhs==fallback_verify.njev*NSTATES); CHECK(fallback_verify.jacobian_checks==0); CHECK(colored.jacobian_colored && verify.jacobian_colored); CHECK(colored.linear_rhs==0 && colored.jacobian_rhs==colored.njev*MODEL_JACOBIAN_COLOR_COUNT); CHECK(colored.jacobian_checks==0); CHECK(verify.linear_rhs==0 && verify.jacobian_rhs==verify.njev*(MODEL_JACOBIAN_COLOR_COUNT+NSTATES)); CHECK(verify.jacobian_checks==verify.njev && verify.jacobian_mismatches==0); CHECK(fallback.accepted==colored.accepted && fallback.rejected==colored.rejected && fallback.njev==colored.njev && fallback.nlu==colored.nlu); CHECK(fallback.accepted==verify.accepted && fallback.rejected==verify.rejected && fallback.njev==verify.njev && fallback.nlu==verify.nlu); CHECK(!memcmp(fallback.final_state,colored.final_state,sizeof(fallback.final_state))); CHECK(!memcmp(fallback.final_state,verify.final_state,sizeof(fallback.final_state))); CHECK(!memcmp(fallback.final_state,fallback_verify.final_state,sizeof(fallback.final_state))); } #if defined(MODEL_JACOBIAN_CANONICAL_RHS) && MODEL_JACOBIAN_CANONICAL_RHS static void canonical_expected(Fixture *f,const double *increments) { double state[NSTATES],base[NSTATES],probe[NSTATES]; memcpy(state,N_VGetArrayPointer(f->y),sizeof(state));CHECK(evaluate(state,base)); for(int j=0;joracle,i,j)=(1.0/increments[j])*(probe[i]-base[i]); } } static void canonical_baseline(void) { Fixture f;fixture_create(&f); const double state[]={.1,.2,.3,.4},weight[]={1,2,3,4}; const double legacy_offset[]={5e5,-7e4,9e3,-1e2}; fixture_set(&f,state,weight,1e5); double original_fy[NSTATES],canonical_fy[NSTATES],increments[NSTATES],wrong_increments[NSTATES]; memcpy(canonical_fy,N_VGetArrayPointer(f.fy),sizeof(canonical_fy)); for(int i=0;i0);CHECK(canonical_count==1 && f.run.jacobian_rhs-before==1); CHECK(f.run.jacobian_fallbacks==1);reject_canonical_base=0; canonical_count=0;before=f.run.jacobian_rhs;poll_calls=0;cancel_poll=1; CHECK(callback(&f)<0);CHECK(canonical_count==0 && f.run.jacobian_rhs==before); CHECK(f.run.jacobian_fallbacks==1);cancel_poll=0; CHECK(f.run.nfev==f.run.jacobian_rhs); CHECK(!memcmp(original_fy,N_VGetArrayPointer(f.fy),sizeof(original_fy))); CHECK(!memcmp(state,N_VGetArrayPointer(f.y),sizeof(state))); canonical_trace=0;fixture_free(&f); } #endif int main(int argc,char **argv) { if(argc!=2)return 64; if(!strcmp(argv[1],"formula"))formula_oracle(); else if(!strcmp(argv[1],"structure"))coloring_validation(); else if(!strcmp(argv[1],"joint-failure"))recoverable_failure(1); else if(!strcmp(argv[1],"individual-failure"))recoverable_failure(2); else if(!strcmp(argv[1],"cancel"))cancellation(); else if(!strcmp(argv[1],"verify"))verify_missing_dependency(); else if(!strcmp(argv[1],"restart"))integration_restart_counters(); #if defined(MODEL_JACOBIAN_CANONICAL_RHS) && MODEL_JACOBIAN_CANONICAL_RHS else if(!strcmp(argv[1],"canonical"))canonical_baseline(); #endif else return 64; printf("{\"case\":\"%s\",\"assertions\":%d,\"passed\":true}\n",argv[1],assertions);return 0; } ''' class NativeJacobianRuntimeTests(unittest.TestCase): @classmethod def setUpClass(cls): if not sys.platform.startswith("linux"): raise unittest.SkipTest("The independent private-ABI oracle currently uses Linux SUNDIALS 7.4 static libraries") if not (SOURCE / "src/cvode/cvode_impl.h").is_file(): raise unittest.SkipTest("The local SUNDIALS 7.4 source tree is required for this private-ABI oracle") compiler, sundials, _ = toolchain() cls.directory = tempfile.TemporaryDirectory(prefix="native-jacobian-runtime-") cls.addClassCleanup(cls.directory.cleanup) directory = Path(cls.directory.name) (directory / "model.h").write_text(MODEL_HEADER) (directory / "harness.c").write_text(HARNESS.replace("@RUNTIME@", str(ROOT / "native/runtime/cvode_solver.c"))) cls.executable = directory / "harness" libraries = [sundials / "lib" / f"libsundials_{name}.a" for name in LIBRARIES] command = [compiler, "-std=c11", "-O3", "-Wall", "-Wextra", "-Werror", "-ffp-contract=off", "-fno-fast-math", "-D_POSIX_C_SOURCE=200809L"] for include in (directory, ROOT / "native/include", sundials / "include", SOURCE / "src/cvode", SOURCE / "src/sundials"): command += ["-I", str(include)] command += [str(directory / "harness.c"), "-Wl,--start-group", *map(str, libraries), "-Wl,--end-group", "-lm", "-o", str(cls.executable)] built = subprocess.run(command, capture_output=True, text=True, timeout=60) if built.returncode: raise AssertionError(built.stdout + built.stderr) cls.canonical_executable = directory / "harness-canonical" canonical_command = command[:-1] + [str(cls.canonical_executable), "-DMODEL_JACOBIAN_CANONICAL_RHS=1"] built = subprocess.run(canonical_command, capture_output=True, text=True, timeout=60) if built.returncode: raise AssertionError(built.stdout + built.stderr) def check_case(self, case, *, canonical=False): executable = self.canonical_executable if canonical else self.executable completed = subprocess.run([str(executable), case], capture_output=True, text=True, timeout=10) self.assertEqual(completed.returncode, 0, completed.stdout + completed.stderr) self.assertIn('"passed":true', completed.stdout) def test_actual_sundials_default_oracle_and_rounding(self): self.check_case("formula") def test_canonical_base_recomputed_original_fy_sets_increments_and_all_calls_count(self): self.check_case("canonical", canonical=True) def test_coloring_structure_and_bounds(self): self.check_case("structure") def test_joint_failure_falls_back_to_complete_dense(self): self.check_case("joint-failure") def test_individual_failure_remains_recoverable(self): self.check_case("individual-failure") def test_cancel_stops_without_fallback_or_phantom_rhs_count(self): self.check_case("cancel") def test_verify_detects_missing_dependency_and_stays_disabled_after_reinit(self): self.check_case("verify") def test_event_and_time_boundary_restart_counter_accounting(self): self.check_case("restart") if __name__ == "__main__": unittest.main()