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 初始化。单张不是只指定三维位置:
|
单张与电影共享 `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
@@ -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] "
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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 |
|
||||||
|
|||||||
Reference in new issue
Block a user