Feat: Add single-frame observer coordinate time option

This commit is contained in:
wyj committed 2026-10-07 21:12:55 -04:00
1 parent b3f1e837d5
commit 69a8cb5647
4 files changed
+69 -11

No files matched your search

+3 -1
View File
@@ -381,7 +381,9 @@ typedef struct {
单张与电影共享 `ObserverState` 和 ray 初始化。单张不是只指定三维位置: 单张与电影共享 `ObserverState` 和 ray 初始化。单张不是只指定三维位置:
由事件、坐标速度、指向与 roll 生成完整四速度和 tetrad。 由事件、坐标速度、指向与 roll 生成完整四速度和 tetrad。
目前解析单张事件取 $t=0$;此构造不要求静态时空或静止观测者。 `--observer-time T` 指定单张相机事件的坐标时(默认 $t=0$,接受任意有限值,
包括负值);metric 求值、ray 初始化与 lens-map 帧元数据使用同一事件时刻。
此构造不要求静态时空或静止观测者;瞬时相机的 proper time 仍以零为参考。
`--observer-position X Y Z` 与 `--look-ra-deg` / `--look-dec-deg` 独立。 `--observer-position X Y Z` 与 `--look-ra-deg` / `--look-dec-deg` 独立。
只有位置时取朝原点的坐标方向;只有指向时令 $\mathbf x=-R\mathbf d$。 只有位置时取朝原点的坐标方向;只有指向时令 $\mathbf x=-R\mathbf d$。
+12 -6
View File
@@ -34,10 +34,10 @@ typedef struct {
double max_cache_psf_flux; double max_cache_psf_flux;
double psf_relative_tail; double psf_relative_tail;
double psf_min_y; double psf_min_y;
double observer_radius; double observer_radius, observer_time;
double observer_position[3], observer_velocity[3], camera_roll_deg; double observer_position[3], observer_velocity[3], camera_roll_deg;
int position_specified, velocity_specified, look_specified; int position_specified, velocity_specified, look_specified;
int radius_specified, roll_specified; int radius_specified, roll_specified, time_specified;
PointSpreadFunction psf; PointSpreadFunction psf;
PsfKernelCache psf_cache; PsfKernelCache psf_cache;
int fast_mode; int fast_mode;
@@ -597,6 +597,9 @@ static int parse_args(int argc, char **argv, Settings *s,
!parse_sensor_bloom_transfer(argv[++i], !parse_sensor_bloom_transfer(argv[++i],
&s->sensor_bloom_transfer)) { &s->sensor_bloom_transfer)) {
s->sensor_bloom_transfer_specified = 1; s->sensor_bloom_transfer_specified = 1;
} else if (!strcmp(argv[i], "--observer-time") && i + 1 < argc &&
!parse_finite(argv[++i], &s->observer_time)) {
s->time_specified = 1;
} else if ((!strcmp(argv[i], "--observer-position") || } else if ((!strcmp(argv[i], "--observer-position") ||
!strcmp(argv[i], "--observer-velocity")) && i + 3 < argc) { !strcmp(argv[i], "--observer-velocity")) && i + 3 < argc) {
const int position = !strcmp(argv[i], "--observer-position"); const int position = !strcmp(argv[i], "--observer-position");
@@ -765,6 +768,7 @@ static void print_help(const char *program) {
" phenomenological approximation, not a specific\n" " phenomenological approximation, not a specific\n"
" CCD/CMOS/Bayer structure; the raw --hdr-output FITS\n" " CCD/CMOS/Bayer structure; the raw --hdr-output FITS\n"
" never includes it.\n" " never includes it.\n"
" --observer-time T Single-frame camera coordinate time (any finite value, default: 0)\n"
" --observer-position X Y Z Coordinate position; alone implies looking at the origin\n" " --observer-position X Y Z Coordinate position; alone implies looking at the origin\n"
" --observer-radius R Infer position = -R * look direction (default R: 30); conflicts with position\n" " --observer-radius R Infer position = -R * look direction (default R: 30); conflicts with position\n"
" --observer-velocity VX VY VZ Coordinate dx/dt, dy/dt, dz/dt (default: 0 0 0); must be timelike\n" " --observer-velocity VX VY VZ Coordinate dx/dt, dy/dt, dz/dt (default: 0 0 0); must be timelike\n"
@@ -1414,7 +1418,8 @@ static int resolve_camera(Settings *s) {
return -1; return -1;
} }
const int camera_specified = s->position_specified || s->look_specified || const int camera_specified = s->position_specified || s->look_specified ||
s->radius_specified || s->velocity_specified || s->roll_specified; s->radius_specified || s->velocity_specified || s->roll_specified ||
s->time_specified;
if (s->position_specified && s->radius_specified) { if (s->position_specified && s->radius_specified) {
fputs("--observer-position and --observer-radius are mutually exclusive.\n", stderr); fputs("--observer-position and --observer-radius are mutually exclusive.\n", stderr);
return -1; return -1;
@@ -1453,7 +1458,8 @@ static int resolve_camera(Settings *s) {
static int build_observer(const Settings *s, const SpacetimeSource *spacetime, static int build_observer(const Settings *s, const SpacetimeSource *spacetime,
ObserverState *observer) { ObserverState *observer) {
ObserverCamera camera = {.look_ra_deg = s->look_ra_deg, ObserverCamera camera = {.coordinate_time = s->observer_time,
.look_ra_deg = s->look_ra_deg,
.look_dec_deg = s->look_dec_deg, .look_dec_deg = s->look_dec_deg,
.roll_deg = s->camera_roll_deg}; .roll_deg = s->camera_roll_deg};
for (int i = 0; i < 3; ++i) { for (int i = 0; i < 3; ++i) {
@@ -1559,7 +1565,7 @@ static int render_observer_frame(const Settings *s, StarCatalog *catalog,
} }
if (s->lens_map_output_path != NULL) { if (s->lens_map_output_path != NULL) {
const LensMapFrame map_frame = {.frame_id = 0, const LensMapFrame map_frame = {.frame_id = 0,
.coordinate_time = 0.0, .coordinate_time = observer->coordinate_time,
.proper_time = 0.0, .proper_time = 0.0,
.mesh = mesh}; .mesh = mesh};
const LensMapProvenance provenance = lens_map_provenance(s, &trace); const LensMapProvenance provenance = lens_map_provenance(s, &trace);
@@ -2307,7 +2313,7 @@ int main(int argc, char **argv) {
"[--tone-map softclip|reinhard] [--tone-map-p P] " "[--tone-map softclip|reinhard] [--tone-map-p P] "
"[--sensor-bloom-limit E --sensor-bloom-transfer e] " "[--sensor-bloom-limit E --sensor-bloom-transfer e] "
"[--observer-radius R | --observer-position X Y Z] " "[--observer-radius R | --observer-position X Y Z] "
"[--observer-velocity VX VY VZ] [--camera-roll-deg ANGLE] " "[--observer-time T] [--observer-velocity VX VY VZ] [--camera-roll-deg ANGLE] "
"[--psf-fwhm-pixels N] [--psf-moffat-beta N] " "[--psf-fwhm-pixels N] [--psf-moffat-beta N] "
"[--max-magnification M] [--max-cache-psf-flux F] " "[--max-magnification M] [--max-cache-psf-flux F] "
"[--psf-relative-tail R] [--psf-min-y Y] " "[--psf-relative-tail R] [--psf-min-y Y] "
+49 -3
View File
@@ -115,14 +115,15 @@ def map_vertices(path):
return values, data[offset:offset + triangles * MAP_TRIANGLE_SIZE] return values, data[offset:offset + triangles * MAP_TRIANGLE_SIZE]
with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory: with tempfile.TemporaryDirectory(prefix='gr-camera-cli-', dir='/tmp/opencode') as directory:
tmp = Path(directory) tmp = Path(directory)
for backend in ('minkowski', 'schwarzschild'): for backend in ('minkowski', 'schwarzschild'):
binary = BUILD / f'{backend}_sky' binary = BUILD / f'{backend}_sky'
help_text = run(binary, '--help').stdout help_text = run(binary, '--help').stdout
ext = 'png' if '.png' in help_text else 'ppm' ext = 'png' if '.png' in help_text else 'ppm'
hdr_available = '--hdr-output' in help_text hdr_available = any(line.startswith(' --hdr-output ')
for option in ('--observer-position', '--observer-velocity', '--camera-roll-deg'): for line in help_text.splitlines())
for option in ('--observer-time', '--observer-position', '--observer-velocity', '--camera-roll-deg'):
assert option in help_text assert option in help_text
assert '--tone-map' in help_text and '--tone-map-p' in help_text assert '--tone-map' in help_text and '--tone-map-p' in help_text
assert '--sensor-bloom-limit' in help_text assert '--sensor-bloom-limit' in help_text
@@ -162,6 +163,15 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
assert pole == render('pole_explicit', '--observer-position', 0, 0, 30, assert pole == render('pole_explicit', '--observer-position', 0, 0, 30,
'--look-ra-deg', 0, '--look-dec-deg', -90) '--look-ra-deg', 0, '--look-dec-deg', -90)
default = render('default') default = render('default')
# Stationary backends are time-translation invariant; the event time
# must nevertheless survive in lens-map metadata, including negatives.
for time in (0, 12.5, -12.5):
timed_map = tmp / f'{backend}_time_{time}.grlens'
assert default == render('timed', '--observer-time', time,
'--lens-map-output', timed_map)
saved_time, proper_time = struct.unpack_from(
'<dd', timed_map.read_bytes(), MAP_FRAME_HEADER_START + 8)
assert saved_time == time and proper_time == 0
pos = (0, 0, 0) if backend == 'minkowski' else (0, 0, 30) pos = (0, 0, 0) if backend == 'minkowski' else (0, 0, 30)
assert default == render('default_explicit', '--observer-position', *pos, assert default == render('default_explicit', '--observer-position', *pos,
'--look-ra-deg', 90, '--look-dec-deg', -90) '--look-ra-deg', 90, '--look-dec-deg', -90)
@@ -221,6 +231,15 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
assert 'PSF cache ready' not in too_long.stderr assert 'PSF cache ready' not in too_long.stderr
errors = [ errors = [
(['--observer-time'], None),
(['--observer-time', ''], None),
(['--observer-time', 'bad'], None),
(['--observer-time', 'nan'], None),
(['--observer-time', 'inf'], None),
(['--observer-time', '-inf'], None),
(['--observer-track', 'missing.csv', '--observer-time', 0], 'cannot be combined'),
(['--frames-dir', tmp, '--observer-time', 0], 'cannot be combined'),
(['--lens-map-input', 'missing.grlens', '--observer-time', 0], 'cannot be combined'),
(['--observer-position', 1, 2], None), (['--observer-position', 1, 2], None),
(['--observer-position', 1, 2, 'nan'], None), (['--observer-position', 1, 2, 'nan'], None),
(['--observer-velocity', 0, 0, 'inf'], None), (['--observer-velocity', 0, 0, 'inf'], None),
@@ -443,3 +462,30 @@ with tempfile.TemporaryDirectory(prefix='gr-camera-cli-') as directory:
f'movie lens map changed with {threads} threads' f'movie lens map changed with {threads} threads'
print('schwarzschild: movie lens map identical with 1, 4 and 16 threads', flush=True) print('schwarzschild: movie lens map identical with 1, 4 and 16 threads', flush=True)
print(f'{backend}: CLI checks passed; single/movie image identical, map max error {max_error:.3g}', flush=True) print(f'{backend}: CLI checks passed; single/movie image identical, map max error {max_error:.3g}', flush=True)
# In the moving bubble, (t, x) -> (t+T, x+v_s*T) preserves the
# metric and physical ray endpoints. This detects a stale t=0 in either
# camera metric evaluation or ray initialization, not just map metadata.
alc = BUILD / 'alcubierre_sky'
if alc.exists():
reference = None
for time in (0, 12.5, -12.5):
path = tmp / f'alcubierre_time_{time}.grlens'
run(alc, *common, '--alcubierre-vs', 0.3,
'--alcubierre-radius', 1, '--observer-time', time,
'--observer-position', 0.3 * time, 0, 0,
'--observer-velocity', 0.3, 0, 0,
'--look-ra-deg', 0, '--look-dec-deg', 0,
'--lens-map-output', path, '--output', tmp / 'alcubierre.png')
assert struct.unpack_from('<d', path.read_bytes(),
MAP_FRAME_HEADER_START + 8)[0] == time
vertices, triangles = map_vertices(path)
if reference is None:
reference = vertices, triangles
continue
expected, expected_triangles = reference
assert len(vertices) == len(expected) and triangles == expected_triangles
for actual, baseline in zip(vertices, expected):
assert actual[9:12] == baseline[9:12]
assert max(abs(a - b) for a, b in zip(actual[:9], baseline[:9])) < 1e-8
print('alcubierre: nonzero camera-time translation checks passed', flush=True)
+5 -1
View File
@@ -18,10 +18,14 @@ for the field and camera.
Both backends accept the same instantaneous camera parameters. Position and Both backends accept the same instantaneous camera parameters. Position and
velocity use the backend's coordinates; velocity means `dx/dt, dy/dt, dz/dt`, velocity use the backend's coordinates; velocity means `dx/dt, dy/dt, dz/dt`,
not a local physical speed. The analytic single-frame event is at `t=0`. not a local physical speed. The single-frame event defaults to `t=0`; use
`--observer-time T` to select another coordinate time (any finite value,
including negative times). Metric evaluation and past-directed ray tracing
start at that event, and saved lens maps retain its coordinate time.
| Option | Meaning / default | | Option | Meaning / default |
| --- | --- | | --- | --- |
| `--observer-time T` | Single-frame camera coordinate time, default `0`; cannot be combined with movie/track or lens-map input |
| `--observer-position X Y Z` | Coordinate position; if look is omitted, point toward the origin | | `--observer-position X Y Z` | Coordinate position; if look is omitted, point toward the origin |
| `--look-ra-deg RA`, `--look-dec-deg DEC` | Coordinate look direction; missing angle defaults to RA=90°, Dec=-90° | | `--look-ra-deg RA`, `--look-dec-deg DEC` | Coordinate look direction; missing angle defaults to RA=90°, Dec=-90° |
| `--observer-radius R` | Positive radius used only to infer position, default 30; conflicts with explicit position | | `--observer-radius R` | Positive radius used only to infer position, default 30; conflicts with explicit position |