From 83dc05fd52ca2dbd5a52b9ac19f7013e96e11d47 Mon Sep 17 00:00:00 2001 From: Yingjie Wang Date: Thu, 27 Aug 2026 04:12:17 -0400 Subject: [PATCH] Observer: parameterize Schwarzschild camera view --- README.md | 8 +++-- src/main.c | 11 ++++-- src/observer.c | 70 +++++++++++++++++++++++++++++++------- src/observer.h | 19 ++++++++--- tests/test_schwarzschild.c | 11 ++++++ 5 files changed, 98 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index bae7cd7..f1e2959 100644 --- a/README.md +++ b/README.md @@ -88,8 +88,12 @@ mkdir -p output/imgs `SPACETIME=minkowski` (the default) and `SPACETIME=schwarzschild` select source files at compile time, so each executable contains exactly one metric provider. -The Schwarzschild demonstration uses mass `M=1`, a static camera at Cartesian -Kerr--Schild position `(30, 0, 0)`, directed at the hole, escapes at `r=256`, +The Schwarzschild demonstration uses mass `M=1` and places a static camera at +coordinate radius `30` by default. `--look-ra-deg` and `--look-dec-deg` +define the direction from the camera to the hole; the camera is placed at the +opposite direction from the origin and its local forward axis points radially +inward. Use `--observer-radius R` to select any `R > 2`; it is a coordinate +radius in Cartesian Kerr--Schild coordinates. The backend escapes at `r=256` and declares capture at `r=1.5`, safely inside the horizon at `r=2`. Those rendering thresholds are Phase-1 demonstration values, not settled production refinement or integration settings. diff --git a/src/main.c b/src/main.c index 0caa9d0..77aa8d0 100644 --- a/src/main.c +++ b/src/main.c @@ -19,6 +19,7 @@ typedef struct { int coarse_cell_pixels; int draw_mesh; double horizontal_fov_deg, look_ra_deg, look_dec_deg, exposure; + double observer_radius; double observer_inward_speed; PointSpreadFunction psf; const char *catalog_path; @@ -102,6 +103,7 @@ static int parse_args(int argc, char **argv, Settings *s, .look_ra_deg = 270.0, .look_dec_deg = 0.0, .exposure = 1e-3, + .observer_radius = 30.0, .psf = {2.7, 4.5}, .catalog_path = "assets/sky_grid_5deg.csv", .output_path = "output/imgs/minkowski_sky.ppm", @@ -137,6 +139,8 @@ static int parse_args(int argc, char **argv, Settings *s, !parse_positive(argv[++i], &s->exposure)) { } else if (!strcmp(argv[i], "--observer-inward-speed") && i + 1 < argc && !parse_speed(argv[++i], &s->observer_inward_speed)) { + } else if (!strcmp(argv[i], "--observer-radius") && i + 1 < argc && + !parse_positive(argv[++i], &s->observer_radius)) { } else if (!strcmp(argv[i], "--psf-fwhm-pixels") && i + 1 < argc && !parse_positive(argv[++i], &s->psf.fwhm_pixels)) { } else if (!strcmp(argv[i], "--psf-moffat-beta") && i + 1 < argc && @@ -184,8 +188,9 @@ static GeodesicTraceConfig trace_config(void) { 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); + return observer_inward_schwarzschild_ks_look_at( + 1.0, s->observer_radius, s->look_ra_deg, s->look_dec_deg, + s->observer_inward_speed, observer); #else *observer = observer_fixed_at_origin_look_at(s->look_ra_deg, s->look_dec_deg); return 0; @@ -352,7 +357,7 @@ int main(int argc, char **argv) { fprintf(stderr, "Usage: %s [--catalog PATH | --all-sky-catalog DIR] [--output PATH] [--width N] [--height " "N] [--fov-deg D] [--look-ra-deg D] [--look-dec-deg D] " - "[--exposure E] [--observer-inward-speed V] " + "[--exposure E] [--observer-radius R] [--observer-inward-speed V] " "[--psf-fwhm-pixels N] [--psf-moffat-beta N] " "[--coarse-cell-pixels N] [--draw-mesh] [--write-catalog PATH] " "[--catalog-load-workers N] " diff --git a/src/observer.c b/src/observer.c index 9552b84..2afd6e4 100644 --- a/src/observer.c +++ b/src/observer.c @@ -30,29 +30,69 @@ ObserverState observer_fixed_at_origin_look_at(double ra_deg, double dec_deg) { {0.0, right[0], right[1], right[2]}}}; } -int observer_static_schwarzschild_ks(double mass, double radius, - ObserverState *out) { - if (out == NULL || mass <= 0.0 || radius <= 2.0 * mass) +static int schwarzschild_look_direction(double ra_deg, double dec_deg, + double direction[3], double up[3], + double right[3]) { + if (!isfinite(ra_deg) || !isfinite(dec_deg) || ra_deg < 0.0 || + ra_deg >= 360.0 || dec_deg < -90.0 || dec_deg > 90.0) + return -1; + const double ra = ra_deg * pi / 180.0; + const double dec = dec_deg * pi / 180.0; + const double cos_ra = cos(ra), sin_ra = sin(ra); + const double cos_dec = cos(dec), sin_dec = sin(dec); + direction[0] = cos_dec * cos_ra; + direction[1] = sin_dec; + direction[2] = cos_dec * sin_ra; + up[0] = -sin_dec * cos_ra; + up[1] = cos_dec; + up[2] = -sin_dec * sin_ra; + right[0] = -sin_ra; + right[1] = 0.0; + right[2] = cos_ra; + return 0; +} + +int observer_static_schwarzschild_ks_look_at(double mass, double radius, + double look_ra_deg, + double look_dec_deg, + ObserverState *out) { + double direction[3], up[3], right[3]; + if (out == NULL || mass <= 0.0 || radius <= 2.0 * mass || + schwarzschild_look_direction(look_ra_deg, look_dec_deg, direction, up, + right)) return -1; const double f = 2.0 * mass / radius; const double normalization = sqrt(1.0 - f); *out = (ObserverState){ .coordinate_time = 0.0, - .coordinate_position = {radius, 0.0, 0.0}, - /* e_(0) is the static four-velocity. e_(1) points inward; its time - * component makes the tetrad orthonormal in the KS metric. */ + .coordinate_position = {-radius * direction[0], -radius * direction[1], + -radius * direction[2]}, + /* e_(0) is the static four-velocity. e_(1) points inward, toward the + * origin; its time component makes the tetrad orthonormal in the KS + * metric. */ .tetrad = {{1.0 / normalization, 0.0, 0.0, 0.0}, - {-f / normalization, -normalization, 0.0, 0.0}, - {0.0, 0.0, 1.0, 0.0}, - {0.0, 0.0, 0.0, 1.0}}}; + {-f / normalization, normalization * direction[0], + normalization * direction[1], normalization * direction[2]}, + {0.0, up[0], up[1], up[2]}, + {0.0, right[0], right[1], right[2]}}}; return 0; } -int observer_inward_schwarzschild_ks(double mass, double radius, - double inward_speed, ObserverState *out) { +int observer_static_schwarzschild_ks(double mass, double radius, + ObserverState *out) { + return observer_static_schwarzschild_ks_look_at(mass, radius, 180.0, 0.0, + out); +} + +int observer_inward_schwarzschild_ks_look_at(double mass, double radius, + double look_ra_deg, + double look_dec_deg, + double inward_speed, + ObserverState *out) { ObserverState static_observer; if (inward_speed < 0.0 || inward_speed >= 1.0 || - observer_static_schwarzschild_ks(mass, radius, &static_observer)) + observer_static_schwarzschild_ks_look_at( + mass, radius, look_ra_deg, look_dec_deg, &static_observer)) return -1; const double gamma = 1.0 / sqrt(1.0 - inward_speed * inward_speed); @@ -65,3 +105,9 @@ int observer_inward_schwarzschild_ks(double mass, double radius, } return 0; } + +int observer_inward_schwarzschild_ks(double mass, double radius, + double inward_speed, ObserverState *out) { + return observer_inward_schwarzschild_ks_look_at(mass, radius, 180.0, 0.0, + inward_speed, out); +} diff --git a/src/observer.h b/src/observer.h index 17aed91..1d9f1d1 100644 --- a/src/observer.h +++ b/src/observer.h @@ -12,12 +12,23 @@ ObserverState observer_fixed_at_origin(void); /* Point the fixed inertial observer at an ICRS-style RA/Dec direction. * The local spatial axes remain (forward, celestial north, increasing RA). */ ObserverState observer_fixed_at_origin_look_at(double ra_deg, double dec_deg); -/* Static camera at (radius, 0, 0) in Cartesian Kerr--Schild coordinates, - * directed toward the Schwarzschild black hole at the origin. */ +/* Static camera at Cartesian Kerr--Schild position -radius * look_direction, + * directed toward the Schwarzschild black hole at the origin. look_direction + * uses the same ICRS-style RA/Dec convention as the flat-space camera. */ +int observer_static_schwarzschild_ks_look_at(double mass, double radius, + double look_ra_deg, + double look_dec_deg, + ObserverState *out); +/* Compatibility shortcut for the +X camera directed toward the origin. */ int observer_static_schwarzschild_ks(double mass, double radius, ObserverState *out); -/* Camera at (radius, 0, 0), moving inward at local speed inward_speed with - * respect to the static Schwarzschild observer. */ +/* As above, with a local radial inward boost relative to the static observer. */ +int observer_inward_schwarzschild_ks_look_at(double mass, double radius, + double look_ra_deg, + double look_dec_deg, + double inward_speed, + ObserverState *out); +/* Compatibility shortcut for the +X camera directed toward the origin. */ int observer_inward_schwarzschild_ks(double mass, double radius, double inward_speed, ObserverState *out); diff --git a/tests/test_schwarzschild.c b/tests/test_schwarzschild.c index 911ab2d..7afcef3 100644 --- a/tests/test_schwarzschild.c +++ b/tests/test_schwarzschild.c @@ -7,6 +7,7 @@ int main(void) { SpacetimeSource spacetime = {0}; MetricData metric; ObserverState observer; + ObserverState oriented_observer; ObserverState inward_observer; const GeodesicTraceConfig trace = {.coordinate_time_step = 0.1, .max_steps = 4096, @@ -17,6 +18,16 @@ int main(void) { !isfinite(metric.alpha) || !isfinite(metric.gamma[0][0]) || !isfinite(metric.K[0][0]) || observer_static_schwarzschild_ks(1.0, 30.0, &observer) || + observer_static_schwarzschild_ks_look_at(1.0, 40.0, 270.0, 30.0, + &oriented_observer) || + fabs(oriented_observer.coordinate_position[0]) > 1e-12 || + fabs(oriented_observer.coordinate_position[1] + 20.0) > 1e-12 || + fabs(oriented_observer.coordinate_position[2] - 20.0 * sqrt(3.0)) > + 1e-12 || + fabs(oriented_observer.tetrad[1][1]) > 1e-12 || + fabs(oriented_observer.tetrad[1][2] - 0.5 * sqrt(0.95)) > 1e-12 || + fabs(oriented_observer.tetrad[1][3] + sqrt(0.95) * sqrt(3.0) / 2.0) > + 1e-12 || observer_inward_schwarzschild_ks(1.0, 30.0, 0.5, &inward_observer) || fabs(inward_observer.tetrad[0][0] -