From c8b321a62ab0a1d31f69b84e110ea4ce5f43828b Mon Sep 17 00:00:00 2001 From: Yingjie Wang Date: Tue, 25 Aug 2026 22:56:10 -0400 Subject: [PATCH] Add observer track PNG movie mode --- .gitignore | 2 +- Makefile | 7 +- README.md | 25 +++++ src/main.c | 175 +++++++++++++++++++++++++------ src/movie.c | 41 ++++++++ src/movie.h | 24 +++++ src/observer_track.c | 204 ++++++++++++++++++++++++++++++++++++ src/observer_track.h | 34 ++++++ tests/test_geodesic.c | 19 ++++ tests/test_observer_track.c | 54 ++++++++++ 10 files changed, 552 insertions(+), 33 deletions(-) create mode 100644 src/movie.c create mode 100644 src/movie.h create mode 100644 src/observer_track.c create mode 100644 src/observer_track.h create mode 100644 tests/test_observer_track.c diff --git a/.gitignore b/.gitignore index b77f006..ec2e111 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /build -/output/imgs +/output /scripts/__pycache__ diff --git a/Makefile b/Makefile index 1a06351..e38a24f 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,7 @@ CORE_MINKOWSKI_SOURCES := $(COMMON_SOURCES) src/spacetime_minkowski.c TEST_TARGET := build/test_geodesic FRAME_TEST_TARGET := build/test_frame SCHWARZSCHILD_TEST_TARGET := build/test_schwarzschild +OBSERVER_TRACK_TEST_TARGET := build/test_observer_track .PHONY: all clean run test minkowski schwarzschild @@ -55,10 +56,14 @@ $(FRAME_TEST_TARGET): tests/test_frame.c $(CORE_MINKOWSKI_SOURCES) | build $(SCHWARZSCHILD_TEST_TARGET): tests/test_schwarzschild.c $(COMMON_SOURCES) src/spacetime_schwarzschild.c | build $(CC) $(CPPFLAGS) $(CFLAGS) $(OPENMP_FLAGS) -Isrc $^ $(LDLIBS) -o $@ -test: $(TEST_TARGET) $(FRAME_TEST_TARGET) $(SCHWARZSCHILD_TEST_TARGET) +$(OBSERVER_TRACK_TEST_TARGET): tests/test_observer_track.c $(COMMON_SOURCES) | build + $(CC) $(CPPFLAGS) $(CFLAGS) $(OPENMP_FLAGS) -Isrc $^ $(LDLIBS) -o $@ + +test: $(TEST_TARGET) $(FRAME_TEST_TARGET) $(SCHWARZSCHILD_TEST_TARGET) $(OBSERVER_TRACK_TEST_TARGET) ./$(TEST_TARGET) ./$(FRAME_TEST_TARGET) ./$(SCHWARZSCHILD_TEST_TARGET) + ./$(OBSERVER_TRACK_TEST_TARGET) clean: rm -rf build diff --git a/README.md b/README.md index 77769ac..50eedf7 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,31 @@ color. The output is a binary PPM at `output/imgs/minkowski_sky.ppm`; it can be inspected by most image viewers or converted to PNG with ImageMagick. +## Movie PNG sequence (Phase A) + +Movie mode consumes a canonical observer-track CSV rather than a fixed camera. +Each row stores coordinate time, proper time, Cartesian position, and the full +four-by-four tetrad (21 columns total). Generate the first reproducible +Minkowski benchmark—two coordinate seconds at 30 fps, accelerating from rest +to about `0.95c`—then render its numbered PNG frames: + +```sh +make clean && make ENABLE_PNG=1 +mkdir -p output/imgs +./build/minkowski_sky --write-minkowski-accel-track output/minkowski_accel_2s.csv \ + --duration 2 --fps 30 --proper-acceleration 1.52 +./build/minkowski_sky --observer-track output/minkowski_accel_2s.csv \ + --frames-dir output/imgs --frames-prefix minkowski_accel \ + --start-time 0 --duration 2 --fps 30 +``` + +This writes `minkowski_accel_000000.png` through +`minkowski_accel_000060.png`. The renderer treats the CSV as its observer +input; the acceleration generator is only a reproducible flat-spacetime test +fixture. The current movie path still renders frames independently while the +time-slab/RayPool scheduler is implemented next, so it must not yet be used +as a performance measurement for numerical-relativity data. + PNG output is optional so the default build has no `libpng` dependency. Build with `make ENABLE_PNG=1`, then select it with a `.png` output path: diff --git a/src/main.c b/src/main.c index 4d7a6d1..a3b4411 100644 --- a/src/main.c +++ b/src/main.c @@ -1,12 +1,17 @@ #include "catalog.h" #include "frame.h" +#include "movie.h" +#include "observer_track.h" #include "optics.h" #include "spacetime.h" #include +#include +#include #include #include #include +#include typedef struct { int width, height; @@ -17,6 +22,12 @@ typedef struct { PointSpreadFunction psf; const char *catalog_path; const char *output_path; + const char *observer_track_path; + const char *frames_dir; + const char *frames_prefix; + const char *write_minkowski_accel_track_path; + double movie_start_time, movie_duration, movie_fps; + double minkowski_proper_acceleration; } Settings; static int parse_int(const char *text, int *value) { @@ -57,6 +68,13 @@ static int parse_positive(const char *text, double *value) { return errno || *end || *value <= 0.0 ? -1 : 0; } +static int parse_nonnegative(const char *text, double *value) { + char *end; + errno = 0; + *value = strtod(text, &end); + return errno || *end || !isfinite(*value) || *value < 0.0 ? -1 : 0; +} + static int parse_speed(const char *text, double *value) { char *end; errno = 0; @@ -73,18 +91,20 @@ static int parse_moffat_beta(const char *text, double *value) { static int parse_args(int argc, char **argv, Settings *s, const char **write_path) { - *s = (Settings){1280, - 720, - 16, - 0, - 30.0, - 270.0, - 0.0, - 100.0, - 0.0, - {2.7, 4.5}, - "assets/sky_grid_5deg.csv", - "output/imgs/minkowski_sky.ppm"}; + *s = (Settings){.width = 1280, + .height = 720, + .coarse_cell_pixels = 16, + .horizontal_fov_deg = 30.0, + .look_ra_deg = 270.0, + .look_dec_deg = 0.0, + .exposure = 100.0, + .psf = {2.7, 4.5}, + .catalog_path = "assets/sky_grid_5deg.csv", + .output_path = "output/imgs/minkowski_sky.ppm", + .frames_prefix = "frame", + .movie_duration = 2.0, + .movie_fps = 30.0, + .minkowski_proper_acceleration = 1.52}; *write_path = NULL; for (int i = 1; i < argc; ++i) { if (!strcmp(argv[i], "--catalog") && i + 1 < argc) @@ -115,35 +135,63 @@ static int parse_args(int argc, char **argv, Settings *s, !parse_moffat_beta(argv[++i], &s->psf.moffat_beta)) { } else if (!strcmp(argv[i], "--write-catalog") && i + 1 < argc) *write_path = argv[++i]; + else if (!strcmp(argv[i], "--observer-track") && i + 1 < argc) + s->observer_track_path = argv[++i]; + else if (!strcmp(argv[i], "--frames-dir") && i + 1 < argc) + s->frames_dir = argv[++i]; + else if (!strcmp(argv[i], "--frames-prefix") && i + 1 < argc) + s->frames_prefix = argv[++i]; + else if (!strcmp(argv[i], "--start-time") && i + 1 < argc && + !parse_nonnegative(argv[++i], &s->movie_start_time)) { + } else if (!strcmp(argv[i], "--duration") && i + 1 < argc && + !parse_nonnegative(argv[++i], &s->movie_duration)) { + } else if (!strcmp(argv[i], "--fps") && i + 1 < argc && + !parse_positive(argv[++i], &s->movie_fps)) { + } else if (!strcmp(argv[i], "--proper-acceleration") && i + 1 < argc && + !parse_nonnegative(argv[++i], + &s->minkowski_proper_acceleration)) { + } else if (!strcmp(argv[i], "--write-minkowski-accel-track") && + i + 1 < argc) + s->write_minkowski_accel_track_path = argv[++i]; else return -1; } return 0; } -static int render_frame(const Settings *s, const StarCatalog *catalog, - const SpacetimeSource *spacetime) { +static GeodesicTraceConfig trace_config(void) { #ifdef SPACETIME_SCHWARZSCHILD - ObserverState observer; - if (observer_inward_schwarzschild_ks(1.0, 30.0, - s->observer_inward_speed, &observer)) - return -1; - const GeodesicTraceConfig trace = {.coordinate_time_step = 0.1, - .max_steps = 4096, - .capture_log_alpha_p0 = 8.0}; + return (GeodesicTraceConfig){.coordinate_time_step = 0.1, + .max_steps = 4096, + .capture_log_alpha_p0 = 8.0}; #else - const ObserverState observer = - observer_fixed_at_origin_look_at(s->look_ra_deg, s->look_dec_deg); - const GeodesicTraceConfig trace = {.coordinate_time_step = 1.0, - .max_steps = 2048}; + return (GeodesicTraceConfig){.coordinate_time_step = 1.0, + .max_steps = 2048}; #endif +} + +static int default_observer(const Settings *s, ObserverState *observer) { +#ifdef SPACETIME_SCHWARZSCHILD + return observer_inward_schwarzschild_ks(1.0, 30.0, + s->observer_inward_speed, observer); +#else + *observer = observer_fixed_at_origin_look_at(s->look_ra_deg, s->look_dec_deg); + return 0; +#endif +} + +static int render_observer_frame(const Settings *s, const StarCatalog *catalog, + const SpacetimeSource *spacetime, + const ObserverState *observer, + const char *output_path) { + const GeodesicTraceConfig trace = trace_config(); FrameLensMesh mesh = {0}; double *hdr = calloc((size_t)s->width * s->height * 3, sizeof *hdr); if (hdr == NULL || frame_lens_mesh_build_coarse(&mesh, s->width, s->height, s->coarse_cell_pixels, s->horizontal_fov_deg) || - frame_lens_mesh_trace(&mesh, spacetime, &observer, &trace)) { + frame_lens_mesh_trace(&mesh, spacetime, observer, &trace)) { frame_lens_mesh_destroy(&mesh); free(hdr); return -1; @@ -152,15 +200,72 @@ static int render_frame(const Settings *s, const StarCatalog *catalog, s->exposure, &s->psf); if (s->draw_mesh) frame_draw_mesh(&mesh, hdr, s->width, s->height, 0.5, 0.5); - int result = write_tonemapped_image(s->output_path, hdr, s->width, s->height); + int result = write_tonemapped_image(output_path, hdr, s->width, s->height); fprintf(stderr, "Rendered %zu images from %zu catalog stars to %s (%s)\n", - images, catalog->count, s->output_path, + images, catalog->count, output_path, result == 0 ? "ok" : "write failed"); frame_lens_mesh_destroy(&mesh); free(hdr); return result; } +static int render_frame(const Settings *s, const StarCatalog *catalog, + const SpacetimeSource *spacetime) { + ObserverState observer; + return default_observer(s, &observer) ? -1 + : render_observer_frame(s, catalog, spacetime, + &observer, s->output_path); +} + +static int frame_output_path(char path[PATH_MAX], const Settings *s, + size_t frame_id) { + struct stat st; + if (s->frames_dir == NULL || s->frames_prefix == NULL || + strchr(s->frames_prefix, '/') != NULL || stat(s->frames_dir, &st) || + !S_ISDIR(st.st_mode)) + return -1; + const int written = snprintf(path, PATH_MAX, "%s/%s_%06zu.png", s->frames_dir, + s->frames_prefix, frame_id); + return written < 0 || written >= PATH_MAX ? -1 : 0; +} + +static int render_movie(const Settings *s, const StarCatalog *catalog, + const SpacetimeSource *spacetime) { + ObserverTrack track = {0}; + Movie movie = {0}; + int result = -1; + if (s->observer_track_path == NULL || + observer_track_load_csv(&track, s->observer_track_path) || + movie_init(&movie, &track, s->movie_start_time, s->movie_duration, + s->movie_fps)) + goto done; + for (size_t i = 0; i < movie.frame_count; ++i) { + char output_path[PATH_MAX]; + if (frame_output_path(output_path, s, movie.frames[i].frame_id) || + render_observer_frame(s, catalog, spacetime, &movie.frames[i].observer, + output_path)) + goto done; + } + result = 0; +done: + movie_destroy(&movie); + observer_track_destroy(&track); + return result; +} + +static int write_minkowski_accel_track(const Settings *s) { + ObserverTrack track = {0}; + const int result = observer_track_generate_minkowski_acceleration( + &track, s->minkowski_proper_acceleration, s->movie_duration, + 1.0 / s->movie_fps) || + observer_track_write_csv(&track, + s->write_minkowski_accel_track_path) + ? -1 + : 0; + observer_track_destroy(&track); + return result; +} + int main(int argc, char **argv) { Settings settings; const char *write_path; @@ -170,14 +275,20 @@ int main(int argc, char **argv) { "N] [--fov-deg D] [--look-ra-deg D] [--look-dec-deg D] " "[--exposure E] [--observer-inward-speed V] " "[--psf-fwhm-pixels N] [--psf-moffat-beta N] " - "[--coarse-cell-pixels N] [--draw-mesh] " - "[--write-catalog PATH]\n", + "[--coarse-cell-pixels N] [--draw-mesh] [--write-catalog PATH] " + "[--observer-track PATH --frames-dir DIR --frames-prefix NAME " + "--start-time T --duration T --fps N] " + "[--proper-acceleration A --write-minkowski-accel-track PATH]\n", argv[0]); return 2; } if (write_path != NULL) return catalog_write_octant_grid(write_path) == 0 ? 0 : (perror(write_path), 1); + if (settings.write_minkowski_accel_track_path != NULL) + return write_minkowski_accel_track(&settings) == 0 + ? 0 + : (perror(settings.write_minkowski_accel_track_path), 1); StarCatalog catalog; if (catalog_load_csv(&catalog, settings.catalog_path)) { if (catalog_write_octant_grid(settings.catalog_path) || @@ -193,7 +304,9 @@ int main(int argc, char **argv) { catalog_destroy(&catalog); return 1; } - int result = render_frame(&settings, &catalog, &spacetime); + int result = settings.frames_dir != NULL + ? render_movie(&settings, &catalog, &spacetime) + : render_frame(&settings, &catalog, &spacetime); spacetime_destroy(&spacetime); catalog_destroy(&catalog); return result == 0 ? 0 : 1; diff --git a/src/movie.c b/src/movie.c new file mode 100644 index 0000000..76c4472 --- /dev/null +++ b/src/movie.c @@ -0,0 +1,41 @@ +#include "movie.h" + +#include +#include +#include + +int movie_init(Movie *movie, const ObserverTrack *track, double start_time, + double duration, double frames_per_second) { + if (movie == NULL || track == NULL || !isfinite(start_time) || + !isfinite(duration) || duration < 0.0 || !isfinite(frames_per_second) || + frames_per_second <= 0.0) + return -1; + *movie = (Movie){0}; + const double frame_intervals = duration * frames_per_second; + const size_t intervals = (size_t)llround(frame_intervals); + if (fabs(frame_intervals - (double)intervals) > 1e-9 || + intervals == SIZE_MAX || intervals + 1 > SIZE_MAX / sizeof *movie->frames) + return -1; + movie->frames = calloc(intervals + 1, sizeof *movie->frames); + if (movie->frames == NULL) + return -1; + movie->frame_count = intervals + 1; + for (size_t i = 0; i < movie->frame_count; ++i) { + MovieFrame *frame = &movie->frames[i]; + frame->frame_id = i; + frame->coordinate_time = start_time + i / frames_per_second; + if (observer_track_interpolate(track, frame->coordinate_time, + &frame->observer, &frame->proper_time)) { + movie_destroy(movie); + return -1; + } + } + return 0; +} + +void movie_destroy(Movie *movie) { + if (movie == NULL) + return; + free(movie->frames); + *movie = (Movie){0}; +} diff --git a/src/movie.h b/src/movie.h new file mode 100644 index 0000000..327ad22 --- /dev/null +++ b/src/movie.h @@ -0,0 +1,24 @@ +#ifndef MOVIE_H +#define MOVIE_H + +#include "observer_track.h" + +#include + +typedef struct { + size_t frame_id; + double coordinate_time; + double proper_time; + ObserverState observer; +} MovieFrame; + +typedef struct { + MovieFrame *frames; + size_t frame_count; +} Movie; + +int movie_init(Movie *movie, const ObserverTrack *track, double start_time, + double duration, double frames_per_second); +void movie_destroy(Movie *movie); + +#endif diff --git a/src/observer_track.c b/src/observer_track.c new file mode 100644 index 0000000..37506b4 --- /dev/null +++ b/src/observer_track.c @@ -0,0 +1,204 @@ +#include "observer_track.h" + +#include +#include +#include +#include +#include +#include + +enum { observer_csv_columns = 21 }; + +static int append_sample(ObserverTrack *track, size_t *capacity, + const ObserverSample *sample) { + if (track->count == *capacity) { + const size_t new_capacity = *capacity ? 2 * *capacity : 64; + if (new_capacity < *capacity || + new_capacity > SIZE_MAX / sizeof *track->samples) + return -1; + ObserverSample *samples = + realloc(track->samples, new_capacity * sizeof *track->samples); + if (samples == NULL) + return -1; + track->samples = samples; + *capacity = new_capacity; + } + track->samples[track->count++] = *sample; + return 0; +} + +static int parse_sample(char *line, ObserverSample *sample) { + double values[observer_csv_columns]; + char *cursor = line; + for (int i = 0; i < observer_csv_columns; ++i) { + char *end; + while (isspace((unsigned char)*cursor)) + ++cursor; + values[i] = strtod(cursor, &end); + if (end == cursor || !isfinite(values[i])) + return -1; + cursor = end; + while (isspace((unsigned char)*cursor)) + ++cursor; + if (i + 1 < observer_csv_columns) { + if (*cursor != ',') + return -1; + ++cursor; + } + } + while (isspace((unsigned char)*cursor)) + ++cursor; + if (*cursor != '\0') + return -1; + sample->coordinate_time = values[0]; + sample->proper_time = values[1]; + for (int i = 0; i < 3; ++i) + sample->coordinate_position[i] = values[2 + i]; + for (int row = 0, value = 5; row < 4; ++row) + for (int column = 0; column < 4; ++column) + sample->tetrad[row][column] = values[value++]; + return 0; +} + +int observer_track_load_csv(ObserverTrack *track, const char *path) { + if (track == NULL || path == NULL) + return -1; + *track = (ObserverTrack){0}; + FILE *file = fopen(path, "r"); + if (file == NULL) + return -1; + char line[4096]; + size_t capacity = 0; + int result = -1; + while (fgets(line, sizeof line, file) != NULL) { + ObserverSample sample; + char *start = line; + while (isspace((unsigned char)*start)) + ++start; + if (*start == '\0' || *start == '#') + continue; + if (track->count == 0 && isalpha((unsigned char)*start)) + continue; + if (parse_sample(start, &sample) || + (track->count && + sample.coordinate_time <= + track->samples[track->count - 1].coordinate_time) || + append_sample(track, &capacity, &sample)) + goto done; + } + if (ferror(file) || track->count == 0) + goto done; + result = 0; +done: + if (fclose(file) != 0) + result = -1; + if (result) + observer_track_destroy(track); + return result; +} + +int observer_track_write_csv(const ObserverTrack *track, const char *path) { + if (track == NULL || track->samples == NULL || track->count == 0 || + path == NULL) + return -1; + FILE *file = fopen(path, "w"); + if (file == NULL) + return -1; + int result = fprintf(file, "t,tau,x,y,z,e0t,e0x,e0y,e0z,e1t,e1x,e1y,e1z," + "e2t,e2x,e2y,e2z,e3t,e3x,e3y,e3z\n") < 0 + ? -1 + : 0; + for (size_t i = 0; result == 0 && i < track->count; ++i) { + const ObserverSample *s = &track->samples[i]; + if (fprintf(file, "%.17g,%.17g,%.17g,%.17g,%.17g", s->coordinate_time, + s->proper_time, s->coordinate_position[0], + s->coordinate_position[1], s->coordinate_position[2]) < 0) + result = -1; + for (int row = 0; result == 0 && row < 4; ++row) + for (int column = 0; column < 4; ++column) + if (fprintf(file, ",%.17g", s->tetrad[row][column]) < 0) + result = -1; + if (result == 0 && fputc('\n', file) == EOF) + result = -1; + } + if (fclose(file) != 0) + result = -1; + return result; +} + +int observer_track_interpolate(const ObserverTrack *track, double coordinate_time, + ObserverState *state, double *proper_time) { + if (track == NULL || track->samples == NULL || track->count == 0 || + state == NULL || !isfinite(coordinate_time) || + coordinate_time < track->samples[0].coordinate_time || + coordinate_time > track->samples[track->count - 1].coordinate_time) + return -1; + size_t high = 0; + while (high < track->count && + track->samples[high].coordinate_time < coordinate_time) + ++high; + const size_t low = high == 0 ? 0 : high - 1; + if (high == track->count) + high = low; + const ObserverSample *a = &track->samples[low]; + const ObserverSample *b = &track->samples[high]; + const double fraction = high == low + ? 0.0 + : (coordinate_time - a->coordinate_time) / + (b->coordinate_time - a->coordinate_time); + state->coordinate_time = coordinate_time; + for (int i = 0; i < 3; ++i) + state->coordinate_position[i] = + a->coordinate_position[i] + + fraction * (b->coordinate_position[i] - a->coordinate_position[i]); + for (int row = 0; row < 4; ++row) + for (int column = 0; column < 4; ++column) + state->tetrad[row][column] = + a->tetrad[row][column] + + fraction * (b->tetrad[row][column] - a->tetrad[row][column]); + if (proper_time != NULL) + *proper_time = a->proper_time + fraction * (b->proper_time - a->proper_time); + return 0; +} + +void observer_track_destroy(ObserverTrack *track) { + if (track == NULL) + return; + free(track->samples); + *track = (ObserverTrack){0}; +} + +int observer_track_generate_minkowski_acceleration(ObserverTrack *track, + double acceleration, + double duration, + double sample_interval) { + if (track == NULL || !isfinite(acceleration) || acceleration < 0.0 || + !isfinite(duration) || duration < 0.0 || !isfinite(sample_interval) || + sample_interval <= 0.0) + return -1; + *track = (ObserverTrack){0}; + size_t capacity = 0; + const size_t intervals = (size_t)ceil(duration / sample_interval); + for (size_t i = 0; i <= intervals; ++i) { + const double t = i == intervals ? duration : i * sample_interval; + const double at = acceleration * t; + const double gamma = sqrt(1.0 + at * at); + const double rapidity = asinh(at); + const double displacement = acceleration == 0.0 ? 0.0 : + (gamma - 1.0) / acceleration; + const double sinh_eta = at; + ObserverSample sample = { + .coordinate_time = t, + .proper_time = acceleration == 0.0 ? t : rapidity / acceleration, + .coordinate_position = {0.0, 0.0, -displacement}, + .tetrad = {{gamma, 0.0, 0.0, -sinh_eta}, + {sinh_eta, 0.0, 0.0, -gamma}, + {0.0, 0.0, 1.0, 0.0}, + {0.0, 1.0, 0.0, 0.0}}}; + if (append_sample(track, &capacity, &sample)) { + observer_track_destroy(track); + return -1; + } + } + return 0; +} diff --git a/src/observer_track.h b/src/observer_track.h new file mode 100644 index 0000000..f978242 --- /dev/null +++ b/src/observer_track.h @@ -0,0 +1,34 @@ +#ifndef OBSERVER_TRACK_H +#define OBSERVER_TRACK_H + +#include "observer.h" + +#include + +typedef struct { + double coordinate_time; + double proper_time; + double coordinate_position[3]; + double tetrad[4][4]; +} ObserverSample; + +typedef struct { + ObserverSample *samples; + size_t count; +} ObserverTrack; + +/* Canonical CSV has 21 columns: t,tau,x,y,z,e0t,e0x,...,e3z. */ +int observer_track_load_csv(ObserverTrack *track, const char *path); +int observer_track_write_csv(const ObserverTrack *track, const char *path); +int observer_track_interpolate(const ObserverTrack *track, double coordinate_time, + ObserverState *state, double *proper_time); +void observer_track_destroy(ObserverTrack *track); + +/* Samples an observer initially at rest at the origin, undergoing constant + * proper acceleration along the fixed observer's forward axis. */ +int observer_track_generate_minkowski_acceleration(ObserverTrack *track, + double acceleration, + double duration, + double sample_interval); + +#endif diff --git a/tests/test_geodesic.c b/tests/test_geodesic.c index ba2556f..8822a5a 100644 --- a/tests/test_geodesic.c +++ b/tests/test_geodesic.c @@ -1,4 +1,5 @@ #include "geodesic.h" +#include "observer_track.h" #include #include @@ -38,6 +39,24 @@ int main(void) { result = result || check_ray(&source, &look_at_ra_zero, (double[]){1.0, 0.0, 0.0}, (double[]){1.0, 0.0, 0.0}); + ObserverTrack accelerated = {0}; + ObserverState final_observer; + if (observer_track_generate_minkowski_acceleration(&accelerated, 1.52, 2.0, + 1.0 / 30.0) || + observer_track_interpolate(&accelerated, 2.0, &final_observer, NULL)) { + result = 1; + } else { + const RayEndpoint forward = geodesic_trace_past( + &source, &final_observer, (double[]){1.0, 0.0, 0.0}, + &(GeodesicTraceConfig){.coordinate_time_step = 0.25, .max_steps = 100}); + const double expected_g = sqrt(1.0 + 3.04 * 3.04) + 3.04; + if (forward.status != RAY_ENDPOINT_ESCAPED || + !nearly_equal(forward.frequency_ratio, expected_g)) { + fputs("accelerated-observer Doppler regression failed\n", stderr); + result = 1; + } + } + observer_track_destroy(&accelerated); spacetime_destroy(&source); return result; } diff --git a/tests/test_observer_track.c b/tests/test_observer_track.c new file mode 100644 index 0000000..5dc911e --- /dev/null +++ b/tests/test_observer_track.c @@ -0,0 +1,54 @@ +#include "movie.h" +#include "observer_track.h" + +#include +#include + +static int nearly_equal(double a, double b) { return fabs(a - b) < 1e-12; } + +int main(void) { + const char *path = "/tmp/gr_raytracing_observer_track.csv"; + ObserverTrack generated = {0}, loaded = {0}; + Movie movie = {0}; + int result = 1; + if (observer_track_generate_minkowski_acceleration(&generated, 1.52, 2.0, + 1.0 / 30.0) || + generated.count != 61 || + !nearly_equal(generated.samples[60].coordinate_time, 2.0) || + !nearly_equal(generated.samples[60].tetrad[0][0], + sqrt(1.0 + 3.04 * 3.04)) || + observer_track_write_csv(&generated, path) || + observer_track_load_csv(&loaded, path) || loaded.count != generated.count || + movie_init(&movie, &loaded, 0.0, 2.0, 30.0) || movie.frame_count != 61 || + !nearly_equal(movie.frames[60].coordinate_time, 2.0) || + !nearly_equal(movie.frames[60].observer.tetrad[0][3], -3.04)) + goto done; + ObserverState interpolated; + double proper_time = 0.0; + if (observer_track_interpolate(&loaded, 1.0, &interpolated, &proper_time) || + !nearly_equal(interpolated.coordinate_position[2], + -(sqrt(1.0 + 1.52 * 1.52) - 1.0) / 1.52) || + !nearly_equal(proper_time, asinh(1.52) / 1.52)) + goto done; + { + FILE *bad = fopen(path, "w"); + ObserverTrack invalid = {0}; + if (bad == NULL) + goto done; + if (fputs("0,0,0\n", bad) < 0 || fclose(bad) != 0) + goto done; + if (observer_track_load_csv(&invalid, path) == 0) { + observer_track_destroy(&invalid); + goto done; + } + } + result = 0; +done: + movie_destroy(&movie); + observer_track_destroy(&loaded); + observer_track_destroy(&generated); + remove(path); + if (result) + fputs("observer-track/movie regression failed\n", stderr); + return result; +}