Add observer track PNG movie mode

This commit is contained in:
wyj committed 2026-08-25 22:56:10 -04:00
1 parent d3dc2e5836
commit c8b321a62a
10 files changed
+552 -33

No files matched your search

+1 -1
View File
@@ -1,3 +1,3 @@
/build
/output/imgs
/output
/scripts/__pycache__
+6 -1
View File
@@ -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
+25
View File
@@ -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:
+144 -31
View File
@@ -1,12 +1,17 @@
#include "catalog.h"
#include "frame.h"
#include "movie.h"
#include "observer_track.h"
#include "optics.h"
#include "spacetime.h"
#include <errno.h>
#include <limits.h>
#include <math.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
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;
+41
View File
@@ -0,0 +1,41 @@
#include "movie.h"
#include <math.h>
#include <stdint.h>
#include <stdlib.h>
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};
}
+24
View File
@@ -0,0 +1,24 @@
#ifndef MOVIE_H
#define MOVIE_H
#include "observer_track.h"
#include <stddef.h>
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
+204
View File
@@ -0,0 +1,204 @@
#include "observer_track.h"
#include <ctype.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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;
}
+34
View File
@@ -0,0 +1,34 @@
#ifndef OBSERVER_TRACK_H
#define OBSERVER_TRACK_H
#include "observer.h"
#include <stddef.h>
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
+19
View File
@@ -1,4 +1,5 @@
#include "geodesic.h"
#include "observer_track.h"
#include <math.h>
#include <stdio.h>
@@ -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;
}
+54
View File
@@ -0,0 +1,54 @@
#include "movie.h"
#include "observer_track.h"
#include <math.h>
#include <stdio.h>
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;
}