Feat: Add single-frame observer coordinate time option
This commit is contained in:
1 parent
b3f1e837d5
commit
69a8cb5647
4 files changed
+69
-11
No files matched your search
@@ -381,7 +381,9 @@ typedef struct {
|
||||
|
||||
单张与电影共享 `ObserverState` 和 ray 初始化。单张不是只指定三维位置:
|
||||
由事件、坐标速度、指向与 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` 独立。
|
||||
只有位置时取朝原点的坐标方向;只有指向时令 $\mathbf x=-R\mathbf d$。
|
||||
|
||||
+12
-6
@@ -34,10 +34,10 @@ typedef struct {
|
||||
double max_cache_psf_flux;
|
||||
double psf_relative_tail;
|
||||
double psf_min_y;
|
||||
double observer_radius;
|
||||
double observer_radius, observer_time;
|
||||
double observer_position[3], observer_velocity[3], camera_roll_deg;
|
||||
int position_specified, velocity_specified, look_specified;
|
||||
int radius_specified, roll_specified;
|
||||
int radius_specified, roll_specified, time_specified;
|
||||
PointSpreadFunction psf;
|
||||
PsfKernelCache psf_cache;
|
||||
int fast_mode;
|
||||
@@ -597,6 +597,9 @@ static int parse_args(int argc, char **argv, Settings *s,
|
||||
!parse_sensor_bloom_transfer(argv[++i],
|
||||
&s->sensor_bloom_transfer)) {
|
||||
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") ||
|
||||
!strcmp(argv[i], "--observer-velocity")) && i + 3 < argc) {
|
||||
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"
|
||||
" CCD/CMOS/Bayer structure; the raw --hdr-output FITS\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-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"
|
||||
@@ -1414,7 +1418,8 @@ static int resolve_camera(Settings *s) {
|
||||
return -1;
|
||||
}
|
||||
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) {
|
||||
fputs("--observer-position and --observer-radius are mutually exclusive.\n", stderr);
|
||||
return -1;
|
||||
@@ -1453,7 +1458,8 @@ static int resolve_camera(Settings *s) {
|
||||
|
||||
static int build_observer(const Settings *s, const SpacetimeSource *spacetime,
|
||||
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,
|
||||
.roll_deg = s->camera_roll_deg};
|
||||
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) {
|
||||
const LensMapFrame map_frame = {.frame_id = 0,
|
||||
.coordinate_time = 0.0,
|
||||
.coordinate_time = observer->coordinate_time,
|
||||
.proper_time = 0.0,
|
||||
.mesh = mesh};
|
||||
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] "
|
||||
"[--sensor-bloom-limit E --sensor-bloom-transfer e] "
|
||||
"[--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] "
|
||||
"[--max-magnification M] [--max-cache-psf-flux F] "
|
||||
"[--psf-relative-tail R] [--psf-min-y Y] "
|
||||
|
||||
@@ -115,14 +115,15 @@ def map_vertices(path):
|
||||
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)
|
||||
for backend in ('minkowski', 'schwarzschild'):
|
||||
binary = BUILD / f'{backend}_sky'
|
||||
help_text = run(binary, '--help').stdout
|
||||
ext = 'png' if '.png' in help_text else 'ppm'
|
||||
hdr_available = '--hdr-output' in help_text
|
||||
for option in ('--observer-position', '--observer-velocity', '--camera-roll-deg'):
|
||||
hdr_available = any(line.startswith(' --hdr-output ')
|
||||
for line in help_text.splitlines())
|
||||
for option in ('--observer-time', '--observer-position', '--observer-velocity', '--camera-roll-deg'):
|
||||
assert option in help_text
|
||||
assert '--tone-map' in help_text and '--tone-map-p' 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,
|
||||
'--look-ra-deg', 0, '--look-dec-deg', -90)
|
||||
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)
|
||||
assert default == render('default_explicit', '--observer-position', *pos,
|
||||
'--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
|
||||
|
||||
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, 'nan'], 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'
|
||||
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)
|
||||
|
||||
# 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)
|
||||
@@ -18,10 +18,14 @@ for the field and camera.
|
||||
|
||||
Both backends accept the same instantaneous camera parameters. Position and
|
||||
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 |
|
||||
| --- | --- |
|
||||
| `--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 |
|
||||
| `--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 |
|
||||
|
||||
Reference in new issue
Block a user