diff --git a/.github/workflows/google-play.yml b/.github/workflows/google-play.yml index 673cfe4..28909a5 100644 --- a/.github/workflows/google-play.yml +++ b/.github/workflows/google-play.yml @@ -59,16 +59,6 @@ jobs: - name: Install native Android toolchain run: sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;29.0.14206865" "cmake;3.22.1" - - name: Test the active holomorphic worker - run: | - cc -std=c11 -Wall -Wextra -Werror \ - -Iandroid/app/src/main/cpp \ - tests/test_holomorphic_walk.c \ - android/app/src/main/cpp/holomorphic_walk.c \ - -pthread -lm \ - -o /tmp/test-holomorphic-walk - /tmp/test-holomorphic-walk - - name: Create disposable validation key run: | keytool -genkeypair -noprompt \ @@ -134,7 +124,7 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Launch and touch the random holomorphic explorer + - name: Launch and touch the Cauchy-field explorer uses: reactivecircus/android-emulator-runner@v2 with: api-level: 34 @@ -152,18 +142,16 @@ jobs: adb logcat -d > analytic-continuation-emulator.log adb exec-out screencap -p > analytic-continuation-emulator.png adb shell pidof -s org.isomorphisms.analyticcontinuation.lasso.dev | tr -d '\r' | grep -Eq '^[0-9]+$' - grep -Fq 'holomorphic field started with 3 workers' analytic-continuation-emulator.log - grep -Fq 'holomorphic field ready:' analytic-continuation-emulator.log + grep -Fq 'Cauchy field ready:' analytic-continuation-emulator.log grep -Fq 'zeros=1 poles=1' analytic-continuation-emulator.log - grep -Fq 'holomorphic field first frame:' analytic-continuation-emulator.log - steps=$(sed -n 's/.*holomorphic field: workers=3 steps=\([0-9][0-9]*\).*/\1/p' analytic-continuation-emulator.log | tail -1); test -n "$steps"; test "$steps" -gt 0 - set -- $(sed -n 's/.*holomorphic field ready: surface=\([0-9][0-9]*\)x\([0-9][0-9]*\).*/\1 \2/p' analytic-continuation-emulator.log | tail -1); width=$1; height=$2; min_side=$width; if [ "$height" -lt "$width" ]; then min_side=$height; fi; pixel_radius=$((42 * min_side / 100)); adb shell input tap "$((width / 2 + pixel_radius / 2))" "$((height / 2 + pixel_radius / 4))" + grep -Fq 'Cauchy field first frame:' analytic-continuation-emulator.log + set -- $(sed -n 's/.*Cauchy field ready: surface=\([0-9][0-9]*\)x\([0-9][0-9]*\).*/\1 \2/p' analytic-continuation-emulator.log | tail -1); width=$1; height=$2; min_side=$width; if [ "$height" -lt "$width" ]; then min_side=$height; fi; pixel_radius=$((42 * min_side / 100)); adb shell input tap "$((width / 2 + pixel_radius / 2))" "$((height / 2 + pixel_radius / 4))" sleep 1 adb logcat -d > analytic-continuation-emulator.log adb exec-out screencap -p > analytic-continuation-emulator.png grep -Fq 'zero added:' analytic-continuation-emulator.log grep -Fq 'count=2' analytic-continuation-emulator.log - ! grep -Eiq 'shader compilation failed|program link failed|eglInitialize failed|could not choose GLES3 EGL config|could not create EGL surface/context|eglMakeCurrent failed|holomorphic field shader uniforms unavailable|FATAL EXCEPTION' analytic-continuation-emulator.log + ! grep -Eiq 'shader compilation failed|program link failed|eglInitialize failed|could not choose GLES3 EGL config|could not create EGL surface/context|eglMakeCurrent failed|Cauchy field shader uniforms unavailable|FATAL EXCEPTION' analytic-continuation-emulator.log - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() diff --git a/.github/workflows/holomorphic-apk.yml b/.github/workflows/holomorphic-apk.yml index 96498e0..52eccc1 100644 --- a/.github/workflows/holomorphic-apk.yml +++ b/.github/workflows/holomorphic-apk.yml @@ -6,7 +6,7 @@ on: - main paths: - 'android/**' - - 'tests/test_holomorphic_walk.c' + - 'tests/**' - '.github/workflows/holomorphic-apk.yml' workflow_dispatch: @@ -15,7 +15,7 @@ permissions: concurrency: group: holomorphic-apk-${{ github.ref }} - cancel-in-progress: false + cancel-in-progress: true jobs: build: @@ -34,38 +34,33 @@ jobs: - name: Install native Android toolchain run: sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;29.0.14206865" "cmake;3.22.1" - - name: Verify random holomorphic architecture + - name: Verify GPU Cauchy-field architecture run: | - grep -Fq '#define HOLOMORPHIC_WALK_WORKER_COUNT 3' android/app/src/main/cpp/holomorphic_walk.h - grep -Fq '#define HOLOMORPHIC_WALK_COEFFICIENT_BUDGET 1.20f' android/app/src/main/cpp/holomorphic_walk.h - grep -Fq '#define SEARCH_CANDIDATES 128' android/app/src/main/cpp/holomorphic_walk.c - grep -Fq 'Re(delta_q) and Im(delta_q) are log-modulus/phase sensitivities' android/app/src/main/cpp/holomorphic_walk.c - grep -Fq 'holomorphic_walk_best_direction' android/app/src/main/cpp/analytic_continuation_random.c - grep -Fq 'const float speed = 0.30f;' android/app/src/main/cpp/analytic_continuation_random.c - grep -Fq 'APP_CMD_LOST_FOCUS' android/app/src/main/cpp/analytic_continuation_random.c - grep -Fq 'PLACEMENT_POLE' android/app/src/main/cpp/analytic_continuation_random.c - grep -Fq 'HOLOMORPHIC_WALK_COEFFICIENT_BUDGET' android/app/src/main/cpp/analytic_continuation_random.c - ! grep -Fq 'pause_control_contains' android/app/src/main/cpp/analytic_continuation_random.c - grep -Fq 'u_holomorphic_coefficients' android/app/src/main/assets/continuation.frag.in - grep -Fq 'vec2 u = z / 3.0;' android/app/src/main/assets/continuation.frag.in + test ! -e android/app/src/main/cpp/holomorphic_walk.c + test ! -e android/app/src/main/cpp/holomorphic_walk.h + test ! -e tests/test_holomorphic_walk.c + grep -Fq 'GLint time_location;' android/app/src/main/cpp/analytic_continuation_random.c + grep -Fq 'glGetUniformLocation(engine->program, "u_time")' android/app/src/main/cpp/analytic_continuation_random.c + grep -Fq 'glUniform1f(engine->time_location, animation_time);' android/app/src/main/cpp/analytic_continuation_random.c + grep -Fq 'if (zoom < 0.1f) zoom = 0.1f;' android/app/src/main/cpp/analytic_continuation_random.c + grep -Fq 'if (zoom > 32.0f) zoom = 32.0f;' android/app/src/main/cpp/analytic_continuation_random.c + grep -Fq '#define SOURCE_COUNT 24' android/app/src/main/assets/continuation.frag.in + grep -Fq 'uniform float u_time;' android/app/src/main/assets/continuation.frag.in + grep -Fq 'vec2 source_position' android/app/src/main/assets/continuation.frag.in + grep -Fq 'vec2 source_weight' android/app/src/main/assets/continuation.frag.in + grep -Fq 'vec2 holomorphic_field' android/app/src/main/assets/continuation.frag.in + grep -Fq 'float base = 1.8 * view_radius;' android/app/src/main/assets/continuation.frag.in + grep -Fq 'float radial = 0.35 * view_radius' android/app/src/main/assets/continuation.frag.in + grep -Fq 'vec2 q = holomorphic_field(z, u_time, view_radius);' android/app/src/main/assets/continuation.frag.in grep -Fq 'log_modulus += q.x;' android/app/src/main/assets/continuation.frag.in grep -Fq 'phase += q.y;' android/app/src/main/assets/continuation.frag.in grep -Fq 'vec3 color = wegert_color_from_phase_log_modulus(phase, log_modulus);' android/app/src/main/assets/continuation.frag.in + ! grep -RniE 'holomorphic_walk|u_holomorphic_coefficients|deformation_velocity|coefficient_budget' android/app/src/main/cpp android/app/src/main/assets/continuation.frag.in ! grep -Fq 'u_paused' android/app/src/main/assets/continuation.frag.in test ! -e android/app/src/main/cpp/analytic_continuation.c ! grep -RniE 'lasso_map|inverse_lasso|dragging_lasso|lasso_coefficients|continuation_path' android/app/src/main/cpp android/app/src/main/assets/continuation.frag.in - - name: Test direction search on host - run: | - cc -std=c11 -Wall -Wextra -Werror \ - -Iandroid/app/src/main/cpp \ - tests/test_holomorphic_walk.c \ - android/app/src/main/cpp/holomorphic_walk.c \ - -pthread -lm \ - -o /tmp/test-holomorphic-walk - /tmp/test-holomorphic-walk - - - name: Build holomorphic-random APK + - name: Build Cauchy-field APK working-directory: android run: ./gradlew --no-daemon :app:assembleDebug @@ -85,6 +80,8 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 25 steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 with: distribution: temurin @@ -106,7 +103,7 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Launch moving soup and exercise zero and pole editing + - name: Launch moving Cauchy soup and exercise zero and pole editing uses: reactivecircus/android-emulator-runner@v2 with: api-level: 34 @@ -123,43 +120,15 @@ jobs: adb shell am start -W -n org.isomorphisms.analyticcontinuation.lasso.dev/org.isomorphisms.analyticcontinuation.ExplorerActivity sleep 4 adb logcat -d > holomorphic-emulator.log - grep -Fq 'holomorphic field started with 3 workers' holomorphic-emulator.log - grep -Fq 'holomorphic field ready:' holomorphic-emulator.log + grep -Fq 'Cauchy field ready:' holomorphic-emulator.log grep -Fq 'zeros=1 poles=1' holomorphic-emulator.log - grep -Fq 'holomorphic field first frame:' holomorphic-emulator.log - steps_before=$(sed -n 's/.*holomorphic field: workers=3 steps=\([0-9][0-9]*\).*/\1/p' holomorphic-emulator.log | tail -1); test -n "$steps_before"; test "$steps_before" -gt 0 - + grep -Fq 'Cauchy field first frame:' holomorphic-emulator.log adb exec-out screencap -p > holomorphic-motion-a.png sleep 6 adb exec-out screencap -p > holomorphic-motion-b.png - adb logcat -d > holomorphic-emulator.log - steps_after_motion=$(sed -n 's/.*holomorphic field: workers=3 steps=\([0-9][0-9]*\).*/\1/p' holomorphic-emulator.log | tail -1); test -n "$steps_after_motion"; test "$steps_after_motion" -gt "$steps_before" - python3 - <<'PY' - from PIL import Image, ImageChops, ImageStat - - first = Image.open('holomorphic-motion-a.png').convert('RGB') - second = Image.open('holomorphic-motion-b.png').convert('RGB') - if first.size != second.size: - raise SystemExit('motion screenshots have different dimensions') - - width, height = first.size - box = ( - int(width * 0.10), - int(height * 0.15), - int(width * 0.90), - int(height * 0.85), - ) - diff = ImageChops.difference(first.crop(box), second.crop(box)) - mean = sum(ImageStat.Stat(diff).mean) / 3.0 - pixels = list(diff.getdata()) - changed = sum(1 for pixel in pixels if max(pixel) >= 8) - changed_fraction = changed / max(len(pixels), 1) - print(f'holomorphic motion mean_abs_rgb={mean:.3f} changed_fraction={changed_fraction:.3f}') - if mean < 1.5 or changed_fraction < 0.10: - raise SystemExit('holomorphic motion is still too visually weak') - PY - - set -- $(sed -n 's/.*holomorphic field ready: surface=\([0-9][0-9]*\)x\([0-9][0-9]*\).*/\1 \2/p' holomorphic-emulator.log | tail -1); width=$1; height=$2; min_side=$width; if [ "$height" -lt "$width" ]; then min_side=$height; fi; placement_radius=$((48 * min_side / 1000)); if [ "$placement_radius" -lt 26 ]; then placement_radius=26; fi; if [ "$placement_radius" -gt 38 ]; then placement_radius=38; fi; zero_control_x=$((placement_radius + 16)); control_y=$((height - placement_radius - 16)); pole_control_x=$((zero_control_x + 2 * placement_radius + 14)); adb shell input tap "$zero_control_x" "$control_y"; adb shell input tap "$((width / 2 - 100))" "$((height / 2 + 80))"; printf '%s %s %s %s\n' "$pole_control_x" "$control_y" "$width" "$height" > /tmp/holomorphic-placement-coordinates + python3 tests/check_cauchy_motion.py holomorphic-motion-a.png holomorphic-motion-b.png + python3 tests/check_cauchy_structure_motion.py holomorphic-motion-a.png holomorphic-motion-b.png + set -- $(sed -n 's/.*Cauchy field ready: surface=\([0-9][0-9]*\)x\([0-9][0-9]*\).*/\1 \2/p' holomorphic-emulator.log | tail -1); width=$1; height=$2; min_side=$width; if [ "$height" -lt "$width" ]; then min_side=$height; fi; placement_radius=$((48 * min_side / 1000)); if [ "$placement_radius" -lt 26 ]; then placement_radius=26; fi; if [ "$placement_radius" -gt 38 ]; then placement_radius=38; fi; zero_control_x=$((placement_radius + 16)); control_y=$((height - placement_radius - 16)); pole_control_x=$((zero_control_x + 2 * placement_radius + 14)); adb shell input tap "$zero_control_x" "$control_y"; adb shell input tap "$((width / 2 - 100))" "$((height / 2 + 80))"; printf '%s %s %s %s\n' "$pole_control_x" "$control_y" "$width" "$height" > /tmp/holomorphic-placement-coordinates sleep 1 read pole_control_x control_y width height < /tmp/holomorphic-placement-coordinates; adb shell input tap "$pole_control_x" "$control_y"; adb shell input tap "$((width / 2 + 120))" "$((height / 2 - 70))" sleep 2 @@ -167,9 +136,7 @@ jobs: grep -Fq 'placement selected: pole' holomorphic-emulator.log grep -Eq 'zero added: .*count=2' holomorphic-emulator.log grep -Eq 'pole added: .*count=2' holomorphic-emulator.log - steps_after_edit=$(sed -n 's/.*holomorphic field: workers=3 steps=\([0-9][0-9]*\).*/\1/p' holomorphic-emulator.log | tail -1); test -n "$steps_after_edit"; test "$steps_after_edit" -gt "$steps_after_motion" - grep -Eq 'holomorphic field: workers=3 steps=[0-9]+ .*zeros=2 poles=2' holomorphic-emulator.log - ! grep -Eiq 'shader compilation failed|program link failed|eglInitialize failed|could not choose GLES3 EGL config|could not create EGL surface/context|eglMakeCurrent failed|holomorphic field shader uniforms unavailable|FATAL EXCEPTION' holomorphic-emulator.log + ! grep -Eiq 'shader compilation failed|program link failed|eglInitialize failed|could not choose GLES3 EGL config|could not create EGL surface/context|eglMakeCurrent failed|Cauchy field shader uniforms unavailable|FATAL EXCEPTION' holomorphic-emulator.log adb exec-out screencap -p > holomorphic-emulator.png - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 diff --git a/.github/workflows/native-apk-release.yml b/.github/workflows/native-apk-release.yml index 4de4f49..5d09027 100644 --- a/.github/workflows/native-apk-release.yml +++ b/.github/workflows/native-apk-release.yml @@ -64,14 +64,10 @@ jobs: android/app/src/main/assets/wegert_color.glsl \ .wegert-upstream/code/wegert_color.glsl python -m unittest discover -s tests -v - cc -std=c11 -Wall -Wextra -Werror \ - -Iandroid/app/src/main/cpp \ - tests/test_holomorphic_walk.c \ - android/app/src/main/cpp/holomorphic_walk.c \ - -pthread -lm \ - -o /tmp/test-holomorphic-walk - /tmp/test-holomorphic-walk test ! -e android/app/src/main/cpp/analytic_continuation.c + test ! -e android/app/src/main/cpp/holomorphic_walk.c + test ! -e android/app/src/main/cpp/holomorphic_walk.h + ! grep -RniE 'holomorphic_walk|u_holomorphic_coefficients|deformation_velocity|coefficient_budget' android/app/src/main/cpp android/app/src/main/assets/continuation.frag.in ! grep -RniE 'lasso_map|inverse_lasso|dragging_lasso|lasso_coefficients|continuation_path' android/app/src/main/cpp android/app/src/main/assets/continuation.frag.in - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 @@ -144,7 +140,7 @@ jobs: sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - - name: Touch the GLES holomorphic explorer + - name: Touch the GLES Cauchy-field explorer uses: ReactiveCircus/android-emulator-runner@4c44018e59b437e86cdfc41da381398f93ed8808 # v2 with: api-level: 34 @@ -158,41 +154,27 @@ jobs: adb install -r "artifacts/analytic-continuation-${RELEASE_VERSION}.apk" adb logcat -c adb shell am start -S -W -n org.isomorphisms.analyticcontinuation.lasso.dev/org.isomorphisms.analyticcontinuation.ExplorerActivity - timeout 20 bash -c 'until adb logcat -d | grep -Fq "holomorphic field ready:"; do sleep 1; done' + timeout 20 bash -c 'until adb logcat -d | grep -Fq "Cauchy field ready:"; do sleep 1; done' sleep 3 adb logcat -d > analytic-continuation-emulator.log adb exec-out screencap -p > analytic-continuation-emulator.png adb shell pidof -s org.isomorphisms.analyticcontinuation.lasso.dev | tr -d '\r' | grep -Eq '^[0-9]+$' - grep -Fq 'holomorphic field started with 3 workers' analytic-continuation-emulator.log - grep -Fq 'holomorphic field ready:' analytic-continuation-emulator.log + grep -Fq 'Cauchy field ready:' analytic-continuation-emulator.log grep -Fq 'zeros=1 poles=1' analytic-continuation-emulator.log - grep -Fq 'holomorphic field first frame:' analytic-continuation-emulator.log - steps_before=$(sed -n 's/.*holomorphic field: workers=3 steps=\([0-9][0-9]*\).*/\1/p' analytic-continuation-emulator.log | tail -1); test -n "$steps_before"; test "$steps_before" -gt 0; printf '%s\n' "$steps_before" > /tmp/holomorphic-steps-before + grep -Fq 'Cauchy field first frame:' analytic-continuation-emulator.log ffmpeg -v error -i analytic-continuation-emulator.png -vf 'signalstats,metadata=print:file=explorer-screen-stats.txt' -frames:v 1 -f null - awk -F= '/lavfi.signalstats.YAVG/ { average = $2 } /lavfi.signalstats.YMAX/ { maximum = $2 } END { exit !(average > 20.0 && maximum > 80.0) }' explorer-screen-stats.txt - set -- $(sed -n 's/.*holomorphic field ready: surface=\([0-9][0-9]*\)x\([0-9][0-9]*\).*/\1 \2/p' analytic-continuation-emulator.log | tail -1); width=$1; height=$2; min_side=$width; if [ "$height" -lt "$width" ]; then min_side=$height; fi; control_radius=$((52 * min_side / 1000)); if [ "$control_radius" -lt 28 ]; then control_radius=28; fi; if [ "$control_radius" -gt 42 ]; then control_radius=42; fi; adb shell input tap "$((control_radius + 16))" "$((control_radius + 16))" - sleep 1 - adb logcat -d > analytic-continuation-emulator.log - grep -Fq 'holomorphic field paused' analytic-continuation-emulator.log - - set -- $(sed -n 's/.*holomorphic field ready: surface=\([0-9][0-9]*\)x\([0-9][0-9]*\).*/\1 \2/p' analytic-continuation-emulator.log | tail -1); width=$1; height=$2; min_side=$width; if [ "$height" -lt "$width" ]; then min_side=$height; fi; placement_radius=$((48 * min_side / 1000)); if [ "$placement_radius" -lt 26 ]; then placement_radius=26; fi; if [ "$placement_radius" -gt 38 ]; then placement_radius=38; fi; zero_control_x=$((placement_radius + 16)); control_y=$((height - placement_radius - 16)); pole_control_x=$((zero_control_x + 2 * placement_radius + 14)); adb shell input tap "$zero_control_x" "$control_y"; adb shell input tap "$((width / 2 - 100))" "$((height / 2 + 80))"; printf '%s %s %s %s\n' "$pole_control_x" "$control_y" "$width" "$height" > /tmp/holomorphic-placement-coordinates + set -- $(sed -n 's/.*Cauchy field ready: surface=\([0-9][0-9]*\)x\([0-9][0-9]*\).*/\1 \2/p' analytic-continuation-emulator.log | tail -1); width=$1; height=$2; min_side=$width; if [ "$height" -lt "$width" ]; then min_side=$height; fi; placement_radius=$((48 * min_side / 1000)); if [ "$placement_radius" -lt 26 ]; then placement_radius=26; fi; if [ "$placement_radius" -gt 38 ]; then placement_radius=38; fi; zero_control_x=$((placement_radius + 16)); control_y=$((height - placement_radius - 16)); pole_control_x=$((zero_control_x + 2 * placement_radius + 14)); adb shell input tap "$zero_control_x" "$control_y"; adb shell input tap "$((width / 2 - 100))" "$((height / 2 + 80))"; printf '%s %s %s %s\n' "$pole_control_x" "$control_y" "$width" "$height" > /tmp/holomorphic-placement-coordinates sleep 1 read pole_control_x control_y width height < /tmp/holomorphic-placement-coordinates; adb shell input tap "$pole_control_x" "$control_y"; adb shell input tap "$((width / 2 + 120))" "$((height / 2 - 70))" - sleep 1 + sleep 2 adb logcat -d > analytic-continuation-emulator.log + adb exec-out screencap -p > analytic-continuation-emulator.png grep -Fq 'placement selected: pole' analytic-continuation-emulator.log grep -Eq 'zero added: .*count=2' analytic-continuation-emulator.log grep -Eq 'pole added: .*count=2' analytic-continuation-emulator.log - - set -- $(sed -n 's/.*holomorphic field ready: surface=\([0-9][0-9]*\)x\([0-9][0-9]*\).*/\1 \2/p' analytic-continuation-emulator.log | tail -1); width=$1; height=$2; min_side=$width; if [ "$height" -lt "$width" ]; then min_side=$height; fi; control_radius=$((52 * min_side / 1000)); if [ "$control_radius" -lt 28 ]; then control_radius=28; fi; if [ "$control_radius" -gt 42 ]; then control_radius=42; fi; adb shell input tap "$((control_radius + 16))" "$((control_radius + 16))" - sleep 3 - adb logcat -d > analytic-continuation-emulator.log - adb exec-out screencap -p > analytic-continuation-emulator.png - grep -Fq 'holomorphic field running' analytic-continuation-emulator.log - steps_before=$(cat /tmp/holomorphic-steps-before); steps_after=$(sed -n 's/.*holomorphic field: workers=3 steps=\([0-9][0-9]*\).*/\1/p' analytic-continuation-emulator.log | tail -1); test -n "$steps_after"; test "$steps_after" -gt "$steps_before" - grep -Eq 'holomorphic field: workers=3 steps=[0-9]+ .*zeros=2 poles=2' analytic-continuation-emulator.log - ! grep -Eiq 'shader compilation failed|program link failed|eglInitialize failed|could not choose GLES3 EGL config|could not create EGL surface/context|eglMakeCurrent failed|holomorphic field shader uniforms unavailable|FATAL EXCEPTION' analytic-continuation-emulator.log + ! grep -Eiq 'shader compilation failed|program link failed|eglInitialize failed|could not choose GLES3 EGL config|could not create EGL surface/context|eglMakeCurrent failed|Cauchy field shader uniforms unavailable|FATAL EXCEPTION' analytic-continuation-emulator.log - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 if: always() @@ -233,7 +215,7 @@ jobs: release_title="Analytic Continuation Android test ${RELEASE_VERSION}" release_notes="Corrected Android test APK from commit ${GITHUB_SHA}. - The launcher opens directly into the live native EGL/OpenGL ES random-holomorphic meromorphic explorer. Explicit zeros and poles remain on the ordinary complex plane while a nonvanishing holomorphic factor moves continuously. CI rendered the flowing field and accepted an explorer touch on this exact APK before publication. + The launcher opens directly into the live native EGL/OpenGL ES meromorphic explorer. Explicit zeros and poles remain on the ordinary complex plane while a GPU-evaluated Cauchy-source factor moves continuously and remains holomorphic and nonzero throughout the visible region. CI rendered the field and accepted explorer interaction on this exact APK before publication. It uses the same repository test-only signing key and legacy debug package identity as earlier GitHub APKs, so it installs as an update. It is not the separately signed Google Play production build." diff --git a/.github/workflows/repeated-root-motion.yml b/.github/workflows/repeated-root-motion.yml new file mode 100644 index 0000000..ce2524a --- /dev/null +++ b/.github/workflows/repeated-root-motion.yml @@ -0,0 +1,112 @@ +name: Repeated-root Cauchy motion + +on: + pull_request: + branches: + - main + paths: + - 'android/**' + - 'tests/**' + - '.github/workflows/repeated-root-motion.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: repeated-root-cauchy-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: '17' + + - uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1 + + - name: Install native Android toolchain + run: sdkmanager "platforms;android-36" "build-tools;36.0.0" "ndk;29.0.14206865" "cmake;3.22.1" + + - name: Build repeated-root test APK + working-directory: android + run: ./gradlew --no-daemon :app:assembleDebug + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: repeated-root-cauchy-apk + path: android/app/build/outputs/apk/debug/app-debug.apk + if-no-files-found: error + + emulate: + needs: build + runs-on: ubuntu-latest + timeout-minutes: 25 + steps: + - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0 + + - uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5.7.0 + with: + distribution: temurin + java-version: '17' + + - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: repeated-root-cauchy-apk + path: artifacts + + - name: Install screenshot comparison support + run: | + sudo apt-get update + sudo apt-get install -y python3-pil + + - name: Enable KVM permissions + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Exercise eight nearly coincident zeros + uses: reactivecircus/android-emulator-runner@v2 + with: + api-level: 34 + target: default + arch: x86_64 + profile: medium_phone + ram-size: 2048M + disable-animations: true + emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -no-snapshot-save -no-metrics + script: | + adb install -r artifacts/app-debug.apk + adb shell settings put secure immersive_mode_confirmations confirmed + adb logcat -c + adb shell am start -W -n org.isomorphisms.analyticcontinuation.lasso.dev/org.isomorphisms.analyticcontinuation.ExplorerActivity + sleep 4 + adb logcat -d > repeated-root-emulator.log + grep -Fq 'Cauchy field ready:' repeated-root-emulator.log + bash tests/seed_repeated_zeros.sh repeated-root-emulator.log /tmp/cauchy-repeated-zero-cluster + sleep 2 + adb logcat -d > repeated-root-emulator.log + grep -Eq 'zero added: .*count=8' repeated-root-emulator.log + adb exec-out screencap -p > repeated-root-motion-a.png + sleep 6 + adb exec-out screencap -p > repeated-root-motion-b.png + python3 tests/check_cauchy_motion.py repeated-root-motion-a.png repeated-root-motion-b.png --exclude-file /tmp/cauchy-repeated-zero-cluster --min-mean 1.0 --min-changed-fraction 0.08 + python3 tests/check_cauchy_structure_motion.py repeated-root-motion-a.png repeated-root-motion-b.png --exclude-file /tmp/cauchy-repeated-zero-cluster + ! grep -Eiq 'shader compilation failed|program link failed|eglInitialize failed|could not choose GLES3 EGL config|could not create EGL surface/context|eglMakeCurrent failed|Cauchy field shader uniforms unavailable|FATAL EXCEPTION' repeated-root-emulator.log + + - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + if: always() + with: + name: repeated-root-cauchy-evidence + path: | + repeated-root-emulator.log + repeated-root-motion-a.png + repeated-root-motion-b.png + if-no-files-found: warn diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 9e88b96..db3ffa9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -25,16 +25,10 @@ jobs: with: python-version: "3.12" - run: python -m unittest discover -s tests -v - - name: Smoke-test three holomorphic direction workers - run: | - cc -std=c11 -Wall -Wextra -Werror \ - -Iandroid/app/src/main/cpp \ - tests/test_holomorphic_walk.c \ - android/app/src/main/cpp/holomorphic_walk.c \ - -pthread -lm \ - -o /tmp/test-holomorphic-walk - /tmp/test-holomorphic-walk - - name: Reject migrated Lacunary machinery in the live app + - name: Reject retired CPU walk and migrated Lacunary machinery run: | test ! -e android/app/src/main/cpp/analytic_continuation.c + test ! -e android/app/src/main/cpp/holomorphic_walk.c + test ! -e android/app/src/main/cpp/holomorphic_walk.h + ! grep -RniE 'holomorphic_walk|u_holomorphic_coefficients|deformation_velocity|deformation_accepted_steps' android/app/src/main/cpp android/app/src/main/assets/continuation.frag.in ! grep -RniE 'lasso_map|inverse_lasso|dragging_lasso|lasso_coefficients|continuation_path' android/app/src/main/cpp android/app/src/main/assets/continuation.frag.in diff --git a/AGENTS.md b/AGENTS.md index 2c068a3..d17a800 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,31 +1,126 @@ # Agent instructions -## Runtime video evidence +## Rendering work is runtime work -When a task asks for a video, MP4, GIF, screen recording, or other moving demonstration of this app or renderer, the video must come from the app actually running. +When a task asks for a screenshot, image, video, MP4, GIF, screen recording, animation, or other visual demonstration of this app or renderer, the evidence must come from the actual software under test running. -For a tagged release, install and launch the APK built from that exact tag. For development work, identify the exact commit/build being run. Capture the live render loop with Android/emulator screen recording or direct frame capture from the running process. +For a tagged release, install and launch the APK built from that exact tag. For development work, identify the exact commit/build being run. A successful build, unit test, shader compile, fixture render, or standalone rendering experiment is useful evidence, but it is not a substitute for running the requested app path. -Do not substitute any of the following for runtime footage: +Do not substitute any of the following while claiming they show app behavior: -- AI-generated still images or generated animation frames; +- AI-generated images or animation frames; +- Python, ffmpeg, HTML/canvas, desktop, or other separately implemented renderers; +- a test harness that merely resembles the app; - cross-fades between screenshots or keyframes; -- interpolation, optical-flow synthesis, pan/zoom, or other fake motion; -- a hand-made reconstruction that merely resembles the app; -- frames produced by a separate renderer while claiming they came from the APK. +- interpolation, optical-flow synthesis, pan/zoom, stabilization, or other invented motion; +- a hand-made reconstruction of the intended visualization; +- frames produced by a different executable or renderer than the one named in the task. -If the requested motion is not present in the current app, change the app, build a new APK, run that build, and record the resulting runtime behavior. Do not manufacture the missing behavior in post-production. +If the requested behavior is not present in the current app, change the app, build a new artifact, run that artifact, and capture the resulting runtime behavior. Do not manufacture missing behavior in post-production. -When the visualization contains poles and zeros/holes, movement is not a type change. A pole remains a pole and a zero/hole remains a zero/hole unless the mathematical model is explicitly being changed. Poles and zeros may wander long distances, cross, and exchange regions while retaining their identities. +## Exact backend provenance -Post-processing is limited to operations that do not invent or alter the demonstrated behavior, such as trimming, container conversion, audio removal, or ordinary encoding/resizing. Do not use transitions to conceal discontinuities between unrelated runs. +Running an APK is necessary but is not by itself proof that the requested implementation path ran. -A runtime video should retain enough provenance to reproduce it: +If a task names a compiler or rendering backend, the visual evidence counts for that backend only when the executed artifact actually contains and selects code produced by that backend. This applies in particular to `idris-shader-backend`, `idric-arm-thumb`, DEX/ART, x86-64, and any GPU-specific path. + +Do not claim a backend passed merely because related source files exist in the APK or repository. Prove the path that executed. In particular: + +- handwritten NDK/Clang C is not evidence for an IdriƧ CPU backend; +- handwritten GLSL is not evidence for `idris-shader-backend`; +- a CPU fallback is not GPU-backend evidence; +- an emulator using SwiftShader is not physical PowerVR evidence; +- successful shader compilation without a live draw is not rendering evidence; +- successful installation without proving the selected runtime path is not backend acceptance. + +When fallback paths exist, make the selected path observable in logs or another runtime receipt. If the requested path cannot be proved, report it as unverified rather than silently substituting another path. + +Keep enough provenance to reproduce visual evidence: - repository and commit/tag; -- APK/build artifact used; -- device or emulator target; -- launch and recording procedure; +- APK/build artifact and, when practical, its SHA-256; +- compiler/backend revision used to generate executable or shader code; +- device/emulator and GPU/renderer identity; +- package/process identity while recording; +- launch procedure; +- runtime evidence of backend selection and fallback status; - raw capture or an unambiguous path to it when practical. -Before presenting a video as evidence of app behavior, verify that the app process was actually running while the demonstrated frames were captured. If the app cannot be run, say so. Do not silently replace runtime evidence with a mockup. \ No newline at end of file +## Motion continuity is an acceptance requirement + +A video can come from the real app and still be wrong. Inspect the runtime motion itself. + +Use one continuous running process for a continuous demonstration. Do not hide resets, unrelated runs, state reloads, or discontinuities with editing. + +Animation and random deformation must evolve persistent state. Do not independently redraw object positions, identities, coefficients, camera state, or deformation parameters on each frame. Randomness may drive a continuous process, but the visible state should be integrated continuously through time. + +Time-step handling must tolerate ordinary frame-time variation. A dropped or delayed frame must not produce an unintended teleport or a large unrelated state update. + +If an algorithm scores candidate directions, sample points, or proposed updates and then applies a different blended or larger increment, the applied motion must itself be validated or continuously interpolated. Sparse candidate scoring does not certify an arbitrary intervening step. + +Before calling a moving render acceptable, inspect enough consecutive frames to catch: + +- teleporting or sudden jumps; +- periodic resets or rerandomization; +- marker identities swapping accidentally; +- camera/viewport jumps not requested by the user; +- animation that freezes while only overlays move; +- background evolution that stops while poles/zeros continue moving; +- discontinuities introduced only when recording starts or loops. + +If the runtime is jumpy, fix the runtime. Do not smooth, interpolate, cross-fade, or conceal the jump in the delivered video. + +## Preserve the established visual language + +Do not change the color pipeline merely to make motion more obvious. + +Unless the task explicitly asks for a color-design change, preserve the established domain-coloring behavior, including hue, saturation, brightness/value, gamma, contrast, and any canonical Wegert color mapping or fixtures. Do not turn up saturation, exposure, contrast, or brightness as an incidental rendering change. + +A mathematical change to the displayed function may naturally change the colors. But for the same function, viewport, settings, and time/state, a backend change should reproduce the same intended image within the numerical precision expected of that backend. + +For renderer/backend work, keep at least one stable reference state or fixture and compare it before and after the change. Treat unexplained global hue, saturation, brightness, contrast, orientation, or viewport drift as a regression until explained. + +## Mathematical identity during motion + +When the visualization contains poles and zeros/holes, movement is not a type change. A pole remains a pole and a zero/hole remains a zero/hole unless the mathematical model is explicitly being changed. + +Poles and zeros may wander long distances, cross paths, and exchange regions while retaining their identities. Their visible trajectories should be continuous when wandering is requested; do not accomplish an exchange by deleting and respawning differently typed markers. + +When the model deliberately multiplies a rational zero/pole factor by a holomorphic nonzero factor, that deformation must not silently create, remove, or retype the preserved zeros and poles in the visible domain. + +If the user asks for the field, soup, or background to evolve while poles/zeros wander, both must evolve together in the running app. Do not freeze one layer and synthesize motion in the other afterward. + +## Presentation video means the visualization, not device chrome + +A runtime video intended as an animation, visual example, artwork, or mathematical demonstration should normally contain the visualization itself, not the surrounding Android interface. + +Unless the task explicitly asks to demonstrate interaction or device UI, exclude from the delivered animation: + +- the Android status bar, clock, battery, network and notification icons; +- Android navigation buttons or gesture/navigation chrome; +- emulator/device frames or black borders that are not part of the rendered viewport; +- app placement controls, tool buttons, menus, debug overlays, touch indicators, or other controls that the viewer cannot use in the finished video; +- transient UI shown only to set up the state before recording. + +Prefer a real runtime presentation/capture mode that hides system bars and app controls while leaving the actual renderer running. If the app does not have such a mode and the requested deliverable is a clean animation, add one or otherwise capture only the app's content surface. A crop that removes only non-content device chrome is acceptable when it does not rescale, distort, recompose, or fabricate the visualization, but runtime hiding is preferable. + +Keep a separate uncropped/raw runtime capture when needed for provenance or debugging. Do not confuse that evidence recording with the user-facing animation. The final presentation artifact should be clean unless the user explicitly asks to see controls or the full device screen. + +## Capture and delivery + +Capture screenshots and moving evidence from the running process. For Android, verify that the installed package remains live during capture and retain logs or equivalent receipts sufficient to establish the executing path. + +Post-processing is limited to operations that do not invent or alter demonstrated behavior, such as trimming, container conversion, audio removal, ordinary encoding/resizing, or removing non-content device chrome as described above. Do not use transitions, frame synthesis, color grading, stabilization, or interpolation to repair the evidence. + +Before presenting a visual artifact as evidence of app behavior, verify all of the following: + +1. the exact requested build was installed/launched; +2. the app process was actually running while the frames were captured; +3. the requested backend path, if any, was actually selected; +4. the capture is from a continuous runtime segment rather than a reconstruction; +5. the motion is visually continuous where continuity is intended; +6. established rendering/color behavior has not changed unintentionally; +7. mathematical object identities and requested simultaneous background motion are preserved; +8. the user-facing animation excludes unrequested system chrome and noninteractive controls. + +If any of these cannot be established, say exactly what remains unverified. Do not convert missing runtime evidence into a pass by using a mockup, alternate renderer, alternate backend, or post-production. \ No newline at end of file diff --git a/README.md b/README.md index 62870cc..d2f03bd 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,63 @@ -# Analytic Continuation +# Holomorphic -Native Android explorer for a meromorphic complex function whose holomorphic freedom stays alive. +Native Android explorer for a meromorphic complex portrait with explicit zeros and poles plus a continuously moving holomorphic field. -The picture is the ordinary Wegert-style complex plane with explicit zeros and poles, multiplied by a continuously varying holomorphic/nonvanishing factor: +The visible picture is ```text f_t(z) = R(z) H_t(z) +H_t(z) = exp(q_t(z)) ``` -`R` carries the visible meromorphic divisor. `H_t` moves through legitimate holomorphic states without introducing accidental zeros or poles. The current experimental family is +`R` carries the user-visible meromorphic divisor. On the experimental `cauchy-field` branch, ```text -H_t(z) = exp(q_t(z)) +q_t(z) = sum_k a_k / (xi_k(t) - z) ``` -with a small polynomial `q_t`; that convenient family is not intended as a complete parameterization of holomorphic functions. The motion changes the mathematical function, not merely the hue. +with every source `xi_k(t)` kept outside a disk containing the complete visible viewport. Consequently `q_t` is holomorphic throughout the visible region and `H_t` is nonzero there. The circles remain zeros and the X marks remain the explicit poles contributed by `R`. + +This is deliberately a visible-region construction, not an entire-function construction. Globally, each `q_t` has poles at its off-screen Cauchy sources and `exp(q_t)` has essential singularities there. The branch therefore changes the whole-plane contract rather than pretending that an off-screen singularity is harmless mathematically. + +## GPU Cauchy field -The backend-independent mathematics is specified in [`docs/holomorphic-mathematical-contract.md`](docs/holomorphic-mathematical-contract.md). In particular, whole-plane perturbations in this repository must be entire; bounded-disc kernels with hidden exterior singularities are historical/local constructions rather than the live whole-plane semantics. +The live deformation is evaluated per fragment. The CPU does not search polynomial coefficient directions or maintain a coefficient budget. It supplies elapsed time plus the ordinary interaction state; the fragment shader synthesizes the moving source positions and evaluates the same time-dependent field at every pixel. -## Direction search +The experiment uses 24 Cauchy sources. Their trajectories now use the smooth orbital geometry promoted on `main`: deterministic starting directions, slow clockwise or counterclockwise orbital motion, smooth radial bending, and slight ellipticity. The source scale is still viewport-relative, with -Randomness may choose local data and motion parameters, but it does not certify holomorphy. The mathematical contract defines canonical least-disturbing directions through normalized reproducing/Riesz representers once the admissible entire function space and the prescribed local value or derivative are fixed. +```text +base = 1.8 * view_radius +radial bend <= 0.35 * view_radius +``` -The current CPU prototype still uses three small search workers and 128 nearby/random coefficient candidates as a provisional computational strategy. That heuristic is not the mathematical definition of the canonical direction. Search snapshots run more slowly than display frames; the fragment shader evaluates the accepted coefficients over the whole visible field. +and ellipticity bounded by 8%. Even at the inward extreme the Euclidean source distance is at least `0.92 * 1.45 = 1.334 * view_radius`, so every Cauchy singularity remains outside the visible disk. -For the current `exp(q)` family, +The complex source weights are deterministic and fixed in time. Visible evolution therefore comes from the orbiting source geometry rather than an independent rotation of the residues. + +For ```text -delta log|H(z)| = Re(delta q(z)) -delta phase(H(z)) = Im(delta q(z)) +f(z) = R(z) exp(q(z)) ``` -so exact phase/log-modulus sensitivities are available without treating RGB or screen-space differences as mathematics. +the shader uses the exact decomposition + +```text +log|f(z)| = log|R(z)| + Re(q(z)) +phase(f(z)) = phase(R(z)) + Im(q(z)) +``` + +and sends those completed values to the canonical Wegert color core. No explicit complex exponential is required. + +## Interaction + +The native explorer retains ordinary zero and pole placement, factor dragging, and pinch zoom. The current experimental zoom range is `0.1` through `32.0`. + +The Cauchy source field is viewport-relative: changing the viewport radius also relocates the hidden source orbits so that the sources remain outside the visible region. Zoom is therefore not mathematically independent of this experimental deformation field. ## Wegert boundary -[Wegert](https://github.com/isomorphismes/wegert) owns reusable phase-portrait behavior and rendering preferences, including the canonical complex-value to Wegert-color mapping and ordinary zero/pole interaction pieces. +[Wegert](https://github.com/isomorphismes/wegert) owns reusable phase-portrait behavior and rendering preferences, including the canonical complex-value-to-Wegert-color mapping and ordinary zero/pole interaction pieces. This repository consumes Wegert's exported coloring core and checks it byte-for-byte against Wegert in CI. Its own responsibility is the evolving holomorphic factor, mathematical evolution, GPU evaluation, and thin Android integration. @@ -43,7 +65,7 @@ The inherited lasso/domain-warp engine has been removed from the live source. Th ## Lacunary boundary -[Lacunary](https://github.com/isomorphismes/lacunary) owns the experiments that change the domain/chart/continuation problem rather than simply changing the holomorphic factor on the ordinary meromorphic plane: +[Lacunary](https://github.com/isomorphismes/lacunary) owns experiments that change the domain/chart/continuation problem rather than simply changing the field on the ordinary visible plane: - lasso and deformed-domain constructions; - overlapping convergence discs and reveal geometry; @@ -52,8 +74,10 @@ The inherited lasso/domain-warp engine has been removed from the live source. Th Reusable historical mathematics from those experiments has been archived there. Git history here still records the old branches, but none of that machinery is part of the shipping explorer. -## Runtime +## Runtime and acceptance The Android project is under `android/`. It uses a C `NativeActivity`, EGL, and OpenGL ES 3. No Python runtime or desktop movie renderer owns the live interaction. -Current checks cover the holomorphic direction search, absence of migrated lasso/disc machinery, and the Wegert color boundary. Android emulator evidence and target-phone GPU evidence remain separate. +The retired CPU holomorphic walk, its worker threads, sample-point direction search, polynomial coefficient uniforms, and coefficient budget are absent from the active Cauchy-field build. Acceptance checks the GPU/time-uniform architecture, the Wegert color boundary, absence of migrated lasso machinery, native APK construction, runtime zero/pole interaction, and live frame-to-frame motion from the running APK. The APK and emulator screenshots are uploaded as build artifacts so the experiment can be judged from actual execution rather than CI status alone. + +The whole-plane entire model remains a legitimate alternative, but it is not the semantics of this branch. diff --git a/android/app/src/main/assets/continuation.frag.in b/android/app/src/main/assets/continuation.frag.in index fca1d10..a4f58fb 100644 --- a/android/app/src/main/assets/continuation.frag.in +++ b/android/app/src/main/assets/continuation.frag.in @@ -5,7 +5,7 @@ precision highp int; /*__WEGERT_COLOR_CORE__*/ #define MAX_FACTORS 32 -#define HOLOMORPHIC_COEFFICIENT_COUNT 5 +#define SOURCE_COUNT 24 in vec2 v_ndc; out vec4 frag_color; @@ -15,26 +15,67 @@ uniform int u_zero_count; uniform int u_pole_count; uniform vec2 u_zero_positions[MAX_FACTORS]; uniform vec2 u_pole_positions[MAX_FACTORS]; -uniform vec2 u_holomorphic_coefficients[HOLOMORPHIC_COEFFICIENT_COUNT]; +uniform float u_time; uniform float u_zoom; uniform int u_placement_kind; -vec2 complex_multiply(vec2 left, vec2 right) { +vec2 complex_divide(vec2 numerator, vec2 denominator) { + float magnitude_squared = max(dot(denominator, denominator), 1.0e-12); return vec2( - left.x * right.x - left.y * right.y, - left.x * right.y + left.y * right.x + (numerator.x * denominator.x + numerator.y * denominator.y) / + magnitude_squared, + (numerator.y * denominator.x - numerator.x * denominator.y) / + magnitude_squared ); } -vec2 holomorphic_q(vec2 z) { - // Keep the normalization fixed in the mathematical plane rather than tied - // to zoom, but make the live entire factor large enough to read on a phone. - vec2 u = z / 3.0; - vec2 power = u; +float hash1(float x) { + return fract(sin(x * 127.1 + 31.7) * 43758.5453123); +} + +float source_orbit_speed(int index) { + float k = float(index); + return mix(0.11, 0.19, hash1(k + 4.37)); +} + +float source_handedness(int index) { + float k = float(index); + return hash1(k + 7.91) < 0.5 ? -1.0 : 1.0; +} + +vec2 source_position(int index, float time, float view_radius) { + float k = float(index); + float initial_angle = 6.2831853 * hash1(k + 0.11); + float angle = initial_angle + + source_handedness(index) * source_orbit_speed(index) * time; + + float bend_phase = 6.2831853 * hash1(k + 11.23); + float base = 1.8 * view_radius; + float radial = 0.35 * view_radius * sin(2.0 * angle + bend_phase); + float radius = base + radial; + + float ellipticity = mix(-0.08, 0.08, hash1(k + 14.67)); + vec2 orbit = vec2( + (1.0 + ellipticity) * cos(angle), + (1.0 - ellipticity) * sin(angle) + ); + + return radius * orbit; +} + +vec2 source_weight(int index) { + float k = float(index); + float amplitude = mix(0.08, 0.35, hash1(k + 17.8)); + float phase = 6.2831853 * hash1(k + 21.4); + return amplitude * vec2(cos(phase), sin(phase)); +} + +vec2 holomorphic_field(vec2 z, float time, float view_radius) { vec2 q = vec2(0.0); - for (int index = 0; index < HOLOMORPHIC_COEFFICIENT_COUNT; ++index) { - q += complex_multiply(u_holomorphic_coefficients[index], power); - power = complex_multiply(power, u); + for (int index = 0; index < SOURCE_COUNT; ++index) { + vec2 source = source_position(index, time, view_radius); + vec2 weight = source_weight(index); + q += complex_divide(weight, source - z); } return q; } @@ -63,6 +104,7 @@ void main() { float pixel_radius = 0.42 * min(u_resolution.x, u_resolution.y) * u_zoom; vec2 pixel = gl_FragCoord.xy - 0.5 * u_resolution; vec2 z = pixel / pixel_radius; + float view_radius = length(0.5 * u_resolution) / pixel_radius; float phase = 0.0; float log_modulus = 0.0; @@ -84,9 +126,12 @@ void main() { log_modulus -= 0.5 * log(radius_squared); } - // H(z) = exp(q(z)) is entire and nonzero, so it changes neither zeros nor - // poles. Re(q) adds to log modulus and Im(q) adds to phase exactly. - vec2 q = holomorphic_q(z); + // q_t is holomorphic throughout the visible region because every Cauchy + // source stays outside its circumscribed view disk. exp(q_t) is therefore + // nonzero on the visible region and preserves the explicit divisor there. + // Globally these sources remain genuine poles of q_t and essential + // singularities of exp(q_t); this is not a whole-plane entire model. + vec2 q = holomorphic_field(z, u_time, view_radius); log_modulus += q.x; phase += q.y; diff --git a/android/app/src/main/cpp/CMakeLists.txt b/android/app/src/main/cpp/CMakeLists.txt index fec7741..e63fd44 100644 --- a/android/app/src/main/cpp/CMakeLists.txt +++ b/android/app/src/main/cpp/CMakeLists.txt @@ -10,7 +10,6 @@ target_include_directories(native_app_glue PUBLIC "${NATIVE_GLUE_DIR}") add_library(analytic_continuation SHARED analytic_continuation_random.c - holomorphic_walk.c ) target_compile_features(analytic_continuation PRIVATE c_std_11) target_compile_options(analytic_continuation PRIVATE -Wall -Wextra -Wpedantic) diff --git a/android/app/src/main/cpp/analytic_continuation_random.c b/android/app/src/main/cpp/analytic_continuation_random.c index d4cd739..6285cf9 100644 --- a/android/app/src/main/cpp/analytic_continuation_random.c +++ b/android/app/src/main/cpp/analytic_continuation_random.c @@ -11,11 +11,8 @@ #include #include #include -#include #include -#include "holomorphic_walk.h" - #define LOG_TAG "AnalyticContinuation" #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) @@ -60,7 +57,7 @@ struct engine { GLint pole_count_location; GLint zero_positions_location; GLint pole_positions_location; - GLint holomorphic_coefficients_location; + GLint time_location; GLint zoom_location; GLint placement_kind_location; @@ -70,14 +67,7 @@ struct engine { int pole_count; enum placement_kind placement_kind; - float holomorphic_coefficients[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - float deformation_velocity[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - double deformation_last_time; - double deformation_last_publish; - double deformation_last_log; - uint64_t deformation_accepted_steps; - bool deformation_workers_started; - bool deformation_direction_ready; + double animation_start_time; bool focused; float zoom; @@ -113,14 +103,7 @@ static void initialize_state(struct engine *engine) { engine->pole_positions[0][1] = 0.0f; engine->placement_kind = PLACEMENT_ZERO; - memset(engine->holomorphic_coefficients, 0, sizeof(engine->holomorphic_coefficients)); - memset(engine->deformation_velocity, 0, sizeof(engine->deformation_velocity)); - engine->deformation_last_time = monotonic_seconds(); - engine->deformation_last_publish = 0.0; - engine->deformation_last_log = 0.0; - engine->deformation_accepted_steps = 0; - engine->deformation_workers_started = false; - engine->deformation_direction_ready = false; + engine->animation_start_time = monotonic_seconds(); engine->focused = false; engine->zoom = 1.0f; @@ -252,20 +235,17 @@ static bool create_renderer(struct engine *engine) { engine->pole_count_location = glGetUniformLocation(engine->program, "u_pole_count"); engine->zero_positions_location = glGetUniformLocation(engine->program, "u_zero_positions[0]"); engine->pole_positions_location = glGetUniformLocation(engine->program, "u_pole_positions[0]"); - engine->holomorphic_coefficients_location = glGetUniformLocation( - engine->program, "u_holomorphic_coefficients[0]" - ); + engine->time_location = glGetUniformLocation(engine->program, "u_time"); engine->zoom_location = glGetUniformLocation(engine->program, "u_zoom"); engine->placement_kind_location = glGetUniformLocation(engine->program, "u_placement_kind"); if ( engine->resolution_location < 0 || engine->zero_count_location < 0 || engine->pole_count_location < 0 || engine->zero_positions_location < 0 || - engine->pole_positions_location < 0 || - engine->holomorphic_coefficients_location < 0 || + engine->pole_positions_location < 0 || engine->time_location < 0 || engine->zoom_location < 0 || engine->placement_kind_location < 0 ) { - LOGE("holomorphic field shader uniforms unavailable"); + LOGE("Cauchy field shader uniforms unavailable"); return false; } @@ -285,7 +265,7 @@ static bool create_renderer(struct engine *engine) { glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); LOGI( - "holomorphic field renderer ready: GL_VERSION=%s GL_RENDERER=%s", + "Cauchy field renderer ready: GL_VERSION=%s GL_RENDERER=%s", glGetString(GL_VERSION), glGetString(GL_RENDERER) ); return true; @@ -331,7 +311,9 @@ static bool initialize_display(struct engine *engine) { eglGetConfigAttrib(display, config, EGL_NATIVE_VISUAL_ID, &format); ANativeWindow_setBuffersGeometry(engine->app->window, 0, 0, format); - EGLSurface surface = eglCreateWindowSurface(display, config, engine->app->window, NULL); + EGLSurface surface = eglCreateWindowSurface( + display, config, engine->app->window, NULL + ); EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, context_attributes); if (surface == EGL_NO_SURFACE || context == EGL_NO_CONTEXT) { LOGE("could not create EGL surface/context: 0x%x", eglGetError()); @@ -369,7 +351,7 @@ static bool initialize_display(struct engine *engine) { glViewport(0, 0, engine->width, engine->height); engine->dirty = true; LOGI( - "holomorphic field ready: surface=%dx%d zeros=%d poles=%d", + "Cauchy field ready: surface=%dx%d zeros=%d poles=%d", engine->width, engine->height, engine->zero_count, engine->pole_count ); return true; @@ -428,6 +410,8 @@ static void draw_frame(struct engine *engine) { return; } + float animation_time = (float)(monotonic_seconds() - engine->animation_start_time); + glUseProgram(engine->program); glUniform2f(engine->resolution_location, (float)engine->width, (float)engine->height); glUniform1i(engine->zero_count_location, engine->zero_count); @@ -438,11 +422,7 @@ static void draw_frame(struct engine *engine) { glUniform2fv( engine->pole_positions_location, MAX_FACTORS, &engine->pole_positions[0][0] ); - glUniform2fv( - engine->holomorphic_coefficients_location, - HOLOMORPHIC_WALK_COEFFICIENT_COUNT, - &engine->holomorphic_coefficients[0][0] - ); + glUniform1f(engine->time_location, animation_time); glUniform1f(engine->zoom_location, engine->zoom); glUniform1i(engine->placement_kind_location, (int)engine->placement_kind); @@ -456,7 +436,7 @@ static void draw_frame(struct engine *engine) { GL_RGBA, GL_UNSIGNED_BYTE, center_pixel ); LOGI( - "holomorphic field first frame: center rgba=%u,%u,%u,%u", + "Cauchy field first frame: center rgba=%u,%u,%u,%u", center_pixel[0], center_pixel[1], center_pixel[2], center_pixel[3] ); engine->logged_first_frame = true; @@ -602,110 +582,6 @@ static void add_factor( LOGI("%s added: z=%.6g%+.6gi count=%d", name, point[0], point[1], *count); } -static void publish_deformation_snapshot(struct engine *engine, double now) { - if (!engine->deformation_workers_started) { - return; - } - if ( - engine->deformation_last_publish == 0.0 || - now - engine->deformation_last_publish >= 0.200 - ) { - holomorphic_walk_publish(engine->holomorphic_coefficients); - engine->deformation_last_publish = now; - } -} - -static void log_holomorphic_state(struct engine *engine, double now, float score) { - if (now - engine->deformation_last_log < 2.0) { - return; - } - LOGI( - "holomorphic field: workers=%d steps=%llu budget=%.4f score=%.6g zeros=%d poles=%d", - HOLOMORPHIC_WALK_WORKER_COUNT, - (unsigned long long)engine->deformation_accepted_steps, - holomorphic_walk_coefficient_budget(engine->holomorphic_coefficients), - score, - engine->zero_count, - engine->pole_count - ); - engine->deformation_last_log = now; -} - -static void advance_holomorphic_function(struct engine *engine) { - double now = monotonic_seconds(); - float dt = (float)(now - engine->deformation_last_time); - engine->deformation_last_time = now; - if (dt <= 0.0f) { - return; - } - if (dt > 0.05f) { - dt = 0.05f; - } - - publish_deformation_snapshot(engine, now); - if (!engine->focused || engine->dragging_factor || engine->pinching) { - return; - } - - float score = 0.0f; - float direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - if ( - engine->deformation_workers_started && - holomorphic_walk_best_direction(direction, &score) - ) { - float blend = 1.0f - expf(-4.0f * dt); - const float speed = 0.30f; - for (int index = 0; index < HOLOMORPHIC_WALK_COEFFICIENT_COUNT; ++index) { - engine->deformation_velocity[index][0] = - (1.0f - blend) * engine->deformation_velocity[index][0] + - blend * speed * direction[index][0]; - engine->deformation_velocity[index][1] = - (1.0f - blend) * engine->deformation_velocity[index][1] + - blend * speed * direction[index][1]; - } - engine->deformation_direction_ready = true; - } - - if (!engine->deformation_direction_ready) { - log_holomorphic_state(engine, now, score); - return; - } - - float candidate[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - for (int index = 0; index < HOLOMORPHIC_WALK_COEFFICIENT_COUNT; ++index) { - candidate[index][0] = - engine->holomorphic_coefficients[index][0] + - dt * engine->deformation_velocity[index][0]; - candidate[index][1] = - engine->holomorphic_coefficients[index][1] + - dt * engine->deformation_velocity[index][1]; - } - - if ( - holomorphic_walk_coefficient_budget(candidate) <= - HOLOMORPHIC_WALK_COEFFICIENT_BUDGET - ) { - memcpy( - engine->holomorphic_coefficients, - candidate, - sizeof(engine->holomorphic_coefficients) - ); - engine->deformation_accepted_steps += 1; - engine->dirty = true; - } else { - for (int index = 0; index < HOLOMORPHIC_WALK_COEFFICIENT_COUNT; ++index) { - engine->deformation_velocity[index][0] *= -0.30f; - engine->deformation_velocity[index][1] *= -0.30f; - } - if (engine->deformation_workers_started) { - holomorphic_walk_publish(engine->holomorphic_coefficients); - engine->deformation_last_publish = now; - } - } - - log_holomorphic_state(engine, now, score); -} - static void clear_gesture(struct engine *engine) { engine->candidate_kind = FACTOR_NONE; engine->candidate_index = -1; @@ -736,8 +612,8 @@ static void update_pinch(struct engine *engine, AInputEvent *event) { if (distance < 8.0f) return; float zoom = engine->pinch_start_zoom * distance / engine->pinch_start_distance; - if (zoom < 0.5f) zoom = 0.5f; - if (zoom > 4.0f) zoom = 4.0f; + if (zoom < 0.1f) zoom = 0.1f; + if (zoom > 32.0f) zoom = 32.0f; if (fabsf(zoom - engine->zoom) > 1.0e-4f) { engine->zoom = zoom; engine->dirty = true; @@ -842,10 +718,6 @@ static int32_t handle_input(struct android_app *app, AInputEvent *event) { ); } clear_gesture(engine); - if (engine->deformation_workers_started) { - holomorphic_walk_publish(engine->holomorphic_coefficients); - engine->deformation_last_publish = monotonic_seconds(); - } return 1; } @@ -884,7 +756,6 @@ static void handle_command(struct android_app *app, int32_t command) { break; case APP_CMD_GAINED_FOCUS: engine->focused = true; - engine->deformation_last_time = monotonic_seconds(); engine->dirty = true; break; case APP_CMD_LOST_FOCUS: @@ -908,18 +779,6 @@ void android_main(struct android_app *app) { }; initialize_state(&engine); - engine.deformation_workers_started = holomorphic_walk_start(); - if (engine.deformation_workers_started) { - holomorphic_walk_publish(engine.holomorphic_coefficients); - engine.deformation_last_publish = monotonic_seconds(); - LOGI( - "holomorphic field started with %d workers", - HOLOMORPHIC_WALK_WORKER_COUNT - ); - } else { - LOGE("holomorphic direction workers unavailable; rendering static meromorphic map"); - } - app->userData = &engine; app->onAppCmd = handle_command; app->onInputEvent = handle_input; @@ -935,17 +794,13 @@ void android_main(struct android_app *app) { source->process(app, source); } if (app->destroyRequested != 0) { - if (engine.deformation_workers_started) { - holomorphic_walk_stop(); - engine.deformation_workers_started = false; - } terminate_display(&engine); return; } - if (engine.display != EGL_NO_DISPLAY && engine.focused) { - advance_holomorphic_function(&engine); - } - if (engine.dirty) { + if ( + engine.display != EGL_NO_DISPLAY && + (engine.focused || engine.dirty) + ) { draw_frame(&engine); } } diff --git a/android/app/src/main/cpp/holomorphic_walk.c b/android/app/src/main/cpp/holomorphic_walk.c deleted file mode 100644 index 43f2f1c..0000000 --- a/android/app/src/main/cpp/holomorphic_walk.c +++ /dev/null @@ -1,399 +0,0 @@ -#include "holomorphic_walk.h" - -#include -#include -#include -#include - -#define SEARCH_CANDIDATES 128 -#define DISTURBANCE_SAMPLE_COUNT 19 - -struct walk_worker { - pthread_t thread; - int index; - uint64_t result_generation; - float direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - float score; -}; - -struct walk_state { - pthread_mutex_t mutex; - pthread_cond_t changed; - bool running; - bool stop; - uint64_t generation; - float coefficients[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - struct walk_worker workers[HOLOMORPHIC_WALK_WORKER_COUNT]; -}; - -static struct walk_state walk = { - .mutex = PTHREAD_MUTEX_INITIALIZER, - .changed = PTHREAD_COND_INITIALIZER -}; - -/* Samples are in the normalized entire coordinate u = z / 6. */ -static const float disturbance_samples[DISTURBANCE_SAMPLE_COUNT][2] = { - { 0.00f, 0.00f}, - { 0.18f, 0.07f}, - {-0.13f, 0.26f}, - { 0.31f, -0.19f}, - {-0.37f, -0.11f}, - { 0.08f, 0.48f}, - { 0.49f, 0.17f}, - {-0.28f, 0.49f}, - {-0.52f, -0.29f}, - { 0.33f, -0.55f}, - { 0.66f, 0.08f}, - {-0.61f, 0.24f}, - { 0.14f, 0.71f}, - {-0.18f, -0.73f}, - { 0.73f, -0.31f}, - {-0.70f, -0.36f}, - { 0.46f, 0.72f}, - {-0.48f, 0.69f}, - { 0.79f, 0.43f} -}; - -static uint32_t random_u32(uint32_t *state) { - uint32_t value = *state; - value ^= value << 13; - value ^= value >> 17; - value ^= value << 5; - *state = value; - return value; -} - -static float random_signed(uint32_t *state) { - return 2.0f * ((float)(random_u32(state) & 0x00ffffffu) / 16777215.0f) - 1.0f; -} - -static void complex_multiply( - float left_x, - float left_y, - float right_x, - float right_y, - float output[2] -) { - output[0] = left_x * right_x - left_y * right_y; - output[1] = left_x * right_y + left_y * right_x; -} - -float holomorphic_walk_coefficient_budget( - const float coefficients[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2] -) { - float budget = 0.0f; - for (int index = 0; index < HOLOMORPHIC_WALK_COEFFICIENT_COUNT; ++index) { - budget += hypotf(coefficients[index][0], coefficients[index][1]); - } - return budget; -} - -static float normalize_direction( - float direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2] -) { - float norm = holomorphic_walk_coefficient_budget(direction); - if (norm < 1.0e-7f) { - return 0.0f; - } - float inverse = 1.0f / norm; - for (int index = 0; index < HOLOMORPHIC_WALK_COEFFICIENT_COUNT; ++index) { - direction[index][0] *= inverse; - direction[index][1] *= inverse; - } - return norm; -} - -static void random_direction( - uint32_t *random_state, - float direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2] -) { - do { - for (int index = 0; index < HOLOMORPHIC_WALK_COEFFICIENT_COUNT; ++index) { - direction[index][0] = random_signed(random_state); - direction[index][1] = random_signed(random_state); - } - } while (normalize_direction(direction) == 0.0f); -} - -/* - * q(u) = c1 u + ... + c5 u^5. - * A coefficient direction d therefore changes the displayed holomorphic factor - * H = exp(q) by - * - * delta log|H| = Re(delta q) - * delta phase(H) = Im(delta q). - * - * These are the analytic sensitivities scored before a direction is accepted. - */ -static void direction_at( - const float direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2], - float u_x, - float u_y, - float delta_q[2], - float delta_q_derivative[2] -) { - delta_q[0] = 0.0f; - delta_q[1] = 0.0f; - delta_q_derivative[0] = 0.0f; - delta_q_derivative[1] = 0.0f; - - float power[2] = {1.0f, 0.0f}; - for (int index = 0; index < HOLOMORPHIC_WALK_COEFFICIENT_COUNT; ++index) { - int degree = index + 1; - float next_power[2]; - complex_multiply(power[0], power[1], u_x, u_y, next_power); - - float term[2]; - complex_multiply( - direction[index][0], direction[index][1], - next_power[0], next_power[1], term - ); - delta_q[0] += term[0]; - delta_q[1] += term[1]; - - complex_multiply( - direction[index][0], direction[index][1], - power[0], power[1], term - ); - delta_q_derivative[0] += (float)degree * term[0]; - delta_q_derivative[1] += (float)degree * term[1]; - - power[0] = next_power[0]; - power[1] = next_power[1]; - } -} - -static float outward_budget_slope( - const float coefficients[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2], - const float direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2] -) { - float slope = 0.0f; - for (int index = 0; index < HOLOMORPHIC_WALK_COEFFICIENT_COUNT; ++index) { - float radius = hypotf(coefficients[index][0], coefficients[index][1]); - if (radius < 1.0e-5f) { - continue; - } - slope += ( - coefficients[index][0] * direction[index][0] + - coefficients[index][1] * direction[index][1] - ) / radius; - } - return slope; -} - -static float disturbance_score( - const float coefficients[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2], - const float direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2] -) { - float score = 0.0f; - for (int sample = 0; sample < DISTURBANCE_SAMPLE_COUNT; ++sample) { - float delta_q[2]; - float delta_q_derivative[2]; - direction_at( - direction, - disturbance_samples[sample][0], disturbance_samples[sample][1], - delta_q, delta_q_derivative - ); - float radius_squared = - disturbance_samples[sample][0] * disturbance_samples[sample][0] + - disturbance_samples[sample][1] * disturbance_samples[sample][1]; - float weight = 0.65f + 0.55f * radius_squared; - - /* Re(delta_q) and Im(delta_q) are log-modulus/phase sensitivities. */ - score += weight * ( - delta_q[0] * delta_q[0] + delta_q[1] * delta_q[1] - ); - score += 0.050f * weight * ( - delta_q_derivative[0] * delta_q_derivative[0] + - delta_q_derivative[1] * delta_q_derivative[1] - ); - } - score /= (float)DISTURBANCE_SAMPLE_COUNT; - - float budget = holomorphic_walk_coefficient_budget(coefficients); - float slope = outward_budget_slope(coefficients, direction); - if (budget > 0.52f && slope > 0.0f) { - float closeness = (budget - 0.52f) / - (HOLOMORPHIC_WALK_COEFFICIENT_BUDGET - 0.52f); - score += 0.7f * closeness * closeness * slope * slope; - } - return score; -} - -static void search_direction( - const float coefficients[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2], - float heading[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2], - float best_direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2], - float *best_score, - uint32_t *random_state -) { - *best_score = INFINITY; - for (int candidate_index = 0; candidate_index < SEARCH_CANDIDATES; ++candidate_index) { - float candidate[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - float fresh[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - random_direction(random_state, fresh); - - float old_weight = candidate_index % 11 == 0 ? 0.0f : 0.82f; - float new_weight = candidate_index % 11 == 0 ? 1.0f : 0.18f; - for (int index = 0; index < HOLOMORPHIC_WALK_COEFFICIENT_COUNT; ++index) { - candidate[index][0] = old_weight * heading[index][0] + new_weight * fresh[index][0]; - candidate[index][1] = old_weight * heading[index][1] + new_weight * fresh[index][1]; - } - if (normalize_direction(candidate) == 0.0f) { - continue; - } - - float score = disturbance_score(coefficients, candidate); - if (score < *best_score) { - *best_score = score; - memcpy(best_direction, candidate, sizeof(candidate)); - } - } - - if (isfinite(*best_score)) { - memcpy( - heading, - best_direction, - sizeof(float) * HOLOMORPHIC_WALK_COEFFICIENT_COUNT * 2u - ); - } -} - -static void *worker_main(void *argument) { - struct walk_worker *worker = argument; - uint64_t seen_generation = 0; - uint32_t random_state = 0x9e3779b9u ^ (0x85ebca6bu * (uint32_t)(worker->index + 1)); - float heading[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - random_direction(&random_state, heading); - - while (true) { - float coefficients[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - uint64_t generation; - - pthread_mutex_lock(&walk.mutex); - while (!walk.stop && walk.generation == seen_generation) { - pthread_cond_wait(&walk.changed, &walk.mutex); - } - if (walk.stop) { - pthread_mutex_unlock(&walk.mutex); - return NULL; - } - generation = walk.generation; - memcpy(coefficients, walk.coefficients, sizeof(coefficients)); - pthread_mutex_unlock(&walk.mutex); - - float direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - float score; - search_direction(coefficients, heading, direction, &score, &random_state); - - pthread_mutex_lock(&walk.mutex); - if (!walk.stop && generation >= worker->result_generation && isfinite(score)) { - worker->result_generation = generation; - worker->score = score; - memcpy(worker->direction, direction, sizeof(direction)); - } - seen_generation = generation; - pthread_mutex_unlock(&walk.mutex); - } -} - -bool holomorphic_walk_start(void) { - pthread_mutex_lock(&walk.mutex); - if (walk.running) { - pthread_mutex_unlock(&walk.mutex); - return true; - } - walk.stop = false; - walk.generation = 0; - memset(walk.coefficients, 0, sizeof(walk.coefficients)); - for (int index = 0; index < HOLOMORPHIC_WALK_WORKER_COUNT; ++index) { - walk.workers[index].index = index; - walk.workers[index].result_generation = 0; - walk.workers[index].score = INFINITY; - } - walk.running = true; - pthread_mutex_unlock(&walk.mutex); - - int created = 0; - for (int index = 0; index < HOLOMORPHIC_WALK_WORKER_COUNT; ++index) { - if (pthread_create( - &walk.workers[index].thread, - NULL, - worker_main, - &walk.workers[index] - ) != 0) { - pthread_mutex_lock(&walk.mutex); - walk.stop = true; - pthread_cond_broadcast(&walk.changed); - pthread_mutex_unlock(&walk.mutex); - for (int joined = 0; joined < created; ++joined) { - pthread_join(walk.workers[joined].thread, NULL); - } - pthread_mutex_lock(&walk.mutex); - walk.running = false; - pthread_mutex_unlock(&walk.mutex); - return false; - } - created += 1; - } - return true; -} - -void holomorphic_walk_stop(void) { - pthread_mutex_lock(&walk.mutex); - if (!walk.running) { - pthread_mutex_unlock(&walk.mutex); - return; - } - walk.stop = true; - pthread_cond_broadcast(&walk.changed); - pthread_mutex_unlock(&walk.mutex); - - for (int index = 0; index < HOLOMORPHIC_WALK_WORKER_COUNT; ++index) { - pthread_join(walk.workers[index].thread, NULL); - } - - pthread_mutex_lock(&walk.mutex); - walk.running = false; - pthread_mutex_unlock(&walk.mutex); -} - -void holomorphic_walk_publish( - const float coefficients[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2] -) { - pthread_mutex_lock(&walk.mutex); - if (walk.running && !walk.stop) { - memcpy(walk.coefficients, coefficients, sizeof(walk.coefficients)); - walk.generation += 1; - pthread_cond_broadcast(&walk.changed); - } - pthread_mutex_unlock(&walk.mutex); -} - -bool holomorphic_walk_best_direction( - float direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2], - float *score -) { - bool found = false; - float best_score = INFINITY; - - pthread_mutex_lock(&walk.mutex); - uint64_t minimum_generation = walk.generation > 2 ? walk.generation - 2 : 1; - for (int index = 0; index < HOLOMORPHIC_WALK_WORKER_COUNT; ++index) { - const struct walk_worker *worker = &walk.workers[index]; - if ( - worker->result_generation >= minimum_generation && - worker->score < best_score - ) { - best_score = worker->score; - memcpy(direction, worker->direction, sizeof(worker->direction)); - found = true; - } - } - pthread_mutex_unlock(&walk.mutex); - - if (found && score != NULL) { - *score = best_score; - } - return found; -} diff --git a/android/app/src/main/cpp/holomorphic_walk.h b/android/app/src/main/cpp/holomorphic_walk.h deleted file mode 100644 index a780e73..0000000 --- a/android/app/src/main/cpp/holomorphic_walk.h +++ /dev/null @@ -1,26 +0,0 @@ -#ifndef ANALYTIC_CONTINUATION_HOLOMORPHIC_WALK_H -#define ANALYTIC_CONTINUATION_HOLOMORPHIC_WALK_H - -#include - -#define HOLOMORPHIC_WALK_COEFFICIENT_COUNT 5 -#define HOLOMORPHIC_WALK_WORKER_COUNT 3 -#define HOLOMORPHIC_WALK_COEFFICIENT_BUDGET 1.20f - -bool holomorphic_walk_start(void); -void holomorphic_walk_stop(void); - -void holomorphic_walk_publish( - const float coefficients[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2] -); - -bool holomorphic_walk_best_direction( - float direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2], - float *score -); - -float holomorphic_walk_coefficient_budget( - const float coefficients[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2] -); - -#endif diff --git a/docs/holomorphic-mathematical-contract.md b/docs/holomorphic-mathematical-contract.md index b83e8f6..f6482c7 100644 --- a/docs/holomorphic-mathematical-contract.md +++ b/docs/holomorphic-mathematical-contract.md @@ -1,272 +1,220 @@ # Holomorphic explorer mathematical contract -This document fixes the mathematics of the live explorer independently of CPU threads, GPU backends, shader languages, and performance experiments. +This document states the mathematics of the live `cauchy-field` experiment independently of the particular GPU and Android implementation. -The renderer may approximate these formulas numerically. It must not redefine the mathematical object in order to fit a particular implementation. +The current contract is **local to the visible region**. It deliberately differs from the stronger requirement that the moving factor be entire on all of `C`. -## 1. The live object +## 1. Live object The explorer displays ```text f_t(z) = R(z) H_t(z) +H_t(z) = exp(q_t(z)) ``` -on the ordinary complex plane. - -`R` carries the explicit finite meromorphic divisor chosen by the user: +where `R` carries the explicit zeros and poles selected by the user: ```text R(z) = gain * product_i (z - a_i)^(m_i) / product_j (z - b_j)^(n_j) ``` -where the `a_i` are zeros, the `b_j` are poles, and multiplicity is explicit. - -The live freedom is a nonvanishing entire factor +The moving field is ```text -H_t(z) = exp(q_t(z)) +q_t(z) = sum_k a_k / (xi_k(t) - z). ``` -with `q_t` entire. +The `xi_k(t)` are Cauchy sources kept outside a disk containing the entire visible viewport. The complex weights `a_k` are deterministic and nonzero; in the current version they are fixed in time so visible evolution comes from source motion. -Therefore every displayed state is meromorphic on the ordinary complex plane and has exactly the finite zeros and poles supplied by `R`. The holomorphic motion cannot create, remove, or move that divisor. +## 2. Visible-region invariant -The current implementation uses a small polynomial `q_t`. That is a finite computational coordinate system, not a claim that all entire functions are finite polynomials. +Let `V` be the visible rectangle in complex coordinates, and let `r_view` be the radius of its circumscribed disk centered at the origin. -## 2. Why `exp(q)` is structural rather than cosmetic - -If two meromorphic functions on `C` have the same finite zeros and poles with the same multiplicities, their quotient has no zeros or poles. When that quotient is entire and nonvanishing, it has an entire logarithm because `C` is simply connected. Thus the quotient can be written as `exp(q)` for an entire `q`. - -So +The orbital source construction uses ```text -explicit divisor * exp(entire freedom) +base radius = 1.8 r_view +radial bend in [-0.35 r_view, +0.35 r_view] +ellipticity in [-0.08, +0.08] ``` -is the natural whole-plane model for this explorer. - -## 3. Whole-plane requirement - -For this repository, `q` must be entire, not merely holomorphic on the current viewport. - -A basis function with a pole outside the screen is still not an admissible whole-plane perturbation. Exponentiating such a function generally turns that pole into an essential singularity, so the result is no longer the intended meromorphic plane with only the explicit poles of `R`. - -Local-domain, chart, lasso, and bounded-disc constructions belong in `isomorphismes/lacunary`. - -## 4. Gauge: do not mistake a global recoloring for mathematical motion - -Adding a complex constant `c` to `q` multiplies the entire portrait by `exp(c)`: +Before elliptic distortion, the radial scale therefore lies in ```text -Re(c) -> global modulus scale -Im(c) -> global phase rotation +1.45 r_view <= radius <= 2.15 r_view. ``` -Those degrees of freedom can be useful controls, but they should not masquerade as interesting local holomorphic motion. - -The present polynomial prototype omits the constant term, equivalently fixing `q(0) = 0`. A future basis may use another explicit gauge, but the gauge must be stated. - -## 5. Exact quantities supplied to Wegert - -There is no mathematical reason for the renderer to evaluate the complex exponential explicitly. - -For +The elliptic scale on either coordinate is never smaller than `0.92`, hence ```text -f(z) = R(z) exp(q(z)) +|xi_k(t)| >= 0.92 * 1.45 r_view = 1.334 r_view > r_view. ``` -we have exactly +Every source remains strictly outside the circumscribed view disk. Hence each kernel ```text -log|f(z)| = log|R(z)| + Re(q(z)) -phase(f(z)) = phase(R(z)) + Im(q(z)) +1 / (xi_k(t) - z) ``` -The mathematical renderer boundary is therefore +is holomorphic on a neighborhood of the visible region. Their finite sum `q_t` is holomorphic there, and ```text -explicit zero/pole contribution -+ Re(q), Im(q) --> phase, log modulus --> canonical Wegert value/color behavior --> interaction overlays +H_t(z) = exp(q_t(z)) ``` -RGB differences and screen derivatives are not substitutes for complex derivatives or holomorphy. +is holomorphic and nonzero there. + +Consequently, **within the visible region**, multiplying by `H_t` neither creates nor removes zeros or poles. The visible divisor is exactly the divisor contributed by `R`. + +## 3. This is not a whole-plane meromorphic model -## 6. Local infinitesimal motion +The off-screen singularities are mathematically real even though they are not drawn. -For an infinitesimal change `delta q`, +Each `q_t` has poles at the `xi_k(t)`. Because the source weights are nonzero, exponentiating `q_t` produces essential singularities at those points. Thus the global function ```text -delta log|H(z)| = Re(delta q(z)) -delta phase(H(z)) = Im(delta q(z)) +R(z) exp(q_t(z)) ``` -These are exact analytic sensitivities. +is not a meromorphic function on all of `C` whose only finite singularities are the explicit poles of `R`. -A local derivative may also be used when the desired perturbation is stated in terms of local slope rather than local value. Which local functional is prescribed is part of the mathematical question; it must not be inferred from GPU convenience. +A whole-plane design would require `q_t` to be entire. That is a different model and remains a legitimate alternative, but it is not the semantics of this branch. -## 7. Canonical least-disturbing direction +No documentation, test, or backend should describe the Cauchy-field construction as entire merely because its singularities are off screen. -A canonical direction does not require a visual-energy heuristic. +## 4. Viewport dependence -Choose a Hilbert space `A` of admissible holomorphic perturbations in which point evaluation is continuous. Let `K(z,a)` be its reproducing kernel. If we require a perturbation `phi` to satisfy +The source radius is defined from `r_view`, so the hidden source configuration is viewport-relative. -```text -phi(a) = 1 -``` +Changing zoom changes `r_view` and therefore changes the source positions used to define `q_t`. In this experimental model, zoom is consequently not a mathematically passive camera operation: it changes the moving field while maintaining the invariant that all Cauchy singularities remain outside the visible region. -then the unique minimum-norm solution is +If a later design requires zoom to leave the mathematical function fixed, source placement will need a different rule. -```text -phi_a(z) = K(z,a) / K(a,a). -``` +## 5. Time evolution -This is the precise meaning of a canonical direction of least holomorphic disturbance for a prescribed local value change. +The source trajectories use the smooth orbital geometry promoted on `main`: -More generally, if the prescribed local datum is a derivative or another continuous linear functional, its Riesz/reproducing representer gives the corresponding unique minimum-norm direction after normalization. +- deterministic initial directions; +- per-source angular speeds between `0.11` and `0.19` radians per second; +- deterministic clockwise or counterclockwise handedness; +- a smooth radial bend tied to orbital phase; +- slight ellipticity bounded by 8%. -Thus the mathematical pipeline can be +The source count is ```text -choose anchor/local datum --> compute its canonical minimum-norm holomorphic representer --> choose a small amplitude/sign/time law --> add that direction to q --> render the resulting exact holomorphic state +SOURCE_COUNT = 24. ``` -The user remains the visual arbiter of anchor selection, amplitude, timing, overlap, persistence, and whether the resulting motion looks good. - -## 8. Historical Bergman-disk construction - -The historical `local-holomorphic-perturbations` experiment used the unit-disc Bergman extremal +The fixed source-weight amplitudes satisfy ```text -phi_a(z) = (1 - |a|^2)^2 / (1 - conjugate(a) z)^2 +0.08 <= |a_k| <= 0.35. ``` -which satisfies `phi_a(a) = 1` and is the minimum Bergman-norm holomorphic function on the disc with that value. - -That correctly demonstrated the canonical-direction idea on a bounded disc. It is not, by itself, the whole-plane basis for this repository: for `a != 0` it has a pole at `1 / conjugate(a)` outside the unit disc, and `exp(phi_a)` would have an essential singularity there. +There is no coefficient search, sample-point disturbance score, accepted-step counter, coefficient budget, or CPU worker ensemble in the mathematical evolution. -Do not revive that hidden singularity merely because the kernel was useful in the old local experiment. +The CPU supplies elapsed time. Every fragment evaluates the same `q_t` at its own complex coordinate `z`. -## 9. Whole-plane reproducing-kernel direction +## 6. Exact quantities supplied to Wegert -If we want the same extremal construction using entire functions, the admissible Hilbert space must itself consist of entire functions. +There is no need to evaluate the complex exponential explicitly for coloring. -A natural candidate is a Bargmann-Fock space with an explicit length scale `s`. Its reproducing kernel has the form +For ```text -K_s(z,a) = exp(z conjugate(a) / s^2) +f(z) = R(z) exp(q(z)), ``` -up to the chosen normalization convention. The value-normalized extremal is therefore +we have exactly ```text -phi_a(z) = exp((z conjugate(a) - |a|^2) / s^2) +log|f(z)| = log|R(z)| + Re(q(z)) +phase(f(z)) = phase(R(z)) + Im(q(z)). ``` -which is entire and satisfies `phi_a(a) = 1`. - -This is a mathematically clean whole-plane candidate for canonical local perturbations. The choice of function-space norm and the scale `s` are modeling choices, not universal aesthetic truths. They should be exposed to visual evaluation rather than smuggled in as GPU constants. - -If the gauge removes constant motion, use the corresponding gauge-fixed subspace or a derivative constraint rather than silently reintroducing the constant mode. - -The exact whole-plane perturbation space is therefore a mathematical/design decision to settle before optimizing a GPU implementation. The invariant that its elements are entire is not optional. - -## 10. What randomness means - -Randomness may choose: - -- an anchor point; -- whether the local datum is phase/value/derivative oriented; -- sign or phase of the infinitesimal change; -- amplitude within an accepted bound; -- lifetime and temporal overlap with other perturbations. - -Randomness does not certify holomorphy. The admitted function space and exact formulas do that. - -The number of CPU workers is not mathematics. Three workers plus a coordinator was a useful implementation shape for a four-thread phone, but another CPU, x86-64 implementation, or GPU backend may schedule the same mathematical descriptors differently. - -## 11. Current polynomial prototype - -The current implementation uses +The renderer boundary is therefore ```text -q(u) = c1 u + c2 u^2 + ... + c5 u^5 -u = z / 6 +explicit zero/pole contribution from R ++ Re(q), Im(q) +-> phase, log modulus +-> canonical Wegert value/color behavior +-> interaction overlays. ``` -and a coefficient envelope +RGB differences and screen derivatives are not substitutes for complex holomorphy. + +## 7. Why the Cauchy family is structurally useful + +The kernels ```text -sum_k |c_k| <= 0.72. +1 / (xi - z) ``` -Because the basis is polynomial, every state is entire regardless of this coefficient bound. The bound is therefore not a holomorphy test. +are not basis-free, but they arise directly from the Cauchy-integral picture of holomorphic functions. Moving exterior source data produces a global, smooth deformation felt at every visible point without fitting a small polynomial coefficient vector on the CPU. -On `|u| <= 1`, the triangle inequality gives +This is the practical reason for the experiment. It does not imply that this finite family parameterizes every holomorphic function on the viewport. -```text -|q(u)| <= sum_k |c_k| <= 0.72. -``` +## 8. Separation of responsibilities -So the bound is usefully interpreted as an amplitude/numerical envelope on the reference disc. Any stronger meaning must be proved separately. +The live mathematical field owns: -The present 128-candidate, three-worker score is a provisional CPU exploration strategy. It is not the definition of the canonical holomorphic direction and must not become part of the mathematical semantics merely because it exists in working code. +- the explicit Cauchy-source formula; +- source trajectories and complex weights; +- the invariant that all hidden sources stay outside the visible region; +- the resulting `q_t`. -## 12. Safe motion descriptors +The GPU owns: -If an implementation publishes a segment +- evaluating the source descriptors efficiently for each fragment; +- producing `Re(q_t)` and `Im(q_t)`; +- combining them with the rational zero/pole contribution. -```text -c(tau) = c0 + tau d, -0 <= tau <= tau_max, -``` +The CPU owns: -then every intermediate `q_tau` remains entire automatically when the basis functions are entire. +- elapsed time; +- visible zero/pole interaction state; +- viewport state; +- Android/EGL integration. -If the accepted coefficient set is convex, such as the current `sum |c_k| <= B` ball, endpoints inside the set imply the whole line segment stays inside that particular bound. +Wegert owns the reusable complex-value / phase / log-modulus to phase-portrait color boundary. -Other numerical, amplitude, derivative, or application-specific bounds must be named separately. Do not call them holomorphy checks. +## 9. Interaction invariants -## 13. Separation of responsibilities +The user-visible circles and X marks retain their mathematical types while moving: -The mathematical evolution engine owns: +- a zero remains a zero; +- a pole remains a pole; +- dragging changes its position in `R`; +- the Cauchy field does not convert one into the other. -- the admissible entire perturbation space; -- canonical local representers/directions; -- random selection of local data when desired; -- amplitude and safe-extent rules; -- the resulting `q_t` or compact descriptor. +Pinch zoom is currently permitted over the range `0.1` through `32.0`. -The GPU/backend owns: +## 10. Acceptance -- numerically evaluating an already-defined mathematical descriptor efficiently; -- producing `Re(q)` and `Im(q)` or equivalent exact quantities; -- preserving stated error bounds. +Before treating a build as evidence for this branch, checks should establish at least: -Wegert owns the reusable rendering preference boundary from complex value / phase / log modulus to the canonical phase portrait. +1. the CPU worker/coefficients architecture is absent from the active build; +2. the shader receives elapsed time and evaluates the Cauchy field per fragment; +3. all synthesized source positions remain outside the circumscribed visible radius; +4. the Cauchy contribution is added to phase/log modulus before the Wegert color boundary; +5. the explicit zero/pole interaction still works; +6. the running APK shows actual frame-to-frame field motion; +7. the APK and runtime screenshots are retained so motion and zoom can be judged from execution rather than inferred from CI; +8. runtime evidence comes from the app actually executing, not from interpolated or reconstructed frames. -The user owns the final visual judgment about what motion is worth keeping. +## 11. Whole-plane entire model as an alternative -## 14. Acceptance before GPU optimization +If the explorer later returns to the stronger requirement that the moving factor be globally nonvanishing entire, then `q_t` must again be entire. Exterior Cauchy poles would not be admissible. -Before backend-specific optimization, host/reference tests should establish at least: +A whole-plane reproducing-kernel model such as a Bargmann-Fock space remains one possible direction. For example, with scale `s`, a value-normalized entire representer can take the form -1. the chosen basis/representers are entire; -2. the canonical representer satisfies its prescribed local value or derivative condition; -3. the claimed minimum-norm property matches the chosen Hilbert space; -4. `exp(q)` never changes the explicit zero/pole divisor; -5. phase/log-modulus updates agree with `Re(q)`/`Im(q)`; -6. any coefficient/amplitude bound is described by the theorem it actually satisfies; -7. safe segments remain inside every claimed convex bound; -8. the current CPU heuristic is clearly labeled as an approximation/scheduling strategy rather than the mathematical definition. +```text +phi_a(z) = exp((z conjugate(a) - |a|^2) / s^2). +``` -Only after those are stable should PowerVR, FP16/FP32, fragment/compute division, register pressure, or other backend details be allowed to influence implementation choices. \ No newline at end of file +That model and the present viewport-relative Cauchy-source model answer different mathematical design questions. They should not be conflated. diff --git a/docs/mumford-pattern-theory.md b/docs/mumford-pattern-theory.md new file mode 100644 index 0000000..9f84f97 --- /dev/null +++ b/docs/mumford-pattern-theory.md @@ -0,0 +1,164 @@ +# Mumford note: test structure, not pixel churn + +This note exists because the Cauchy-field work exposed a real failure in a naive +notion of motion. + +With one simple zero/pole configuration, raw screenshot difference looked like a +reasonable proxy for a moving field. With several nearly coincident zeros, an image +can accumulate enormous RGB change while the large visible forms appear much less +mobile. A large changed-pixel count can therefore answer the wrong question. + +The reference to return to is: + +- David Mumford and Agnes Desolneux, *Pattern Theory: The Stochastic Analysis of + Real-World Signals* (A K Peters/CRC Press, 2010), especially the image chapters on + cartoon/texture separation, texture statistics, deformation, and multiscale + analysis. + +## Working distinction + +Do not conflate these three claims: + +1. **The mathematical field moves.** + Test the Cauchy contribution `q_t(z)` directly, before roots, poles, or coloring. +2. **The rendered pixels change.** + RGB difference can remain a useful smoke test. +3. **Large-scale rendered structure moves.** + This needs a geometric, multiscale test. It is the property a person means when + saying that the "soup" visibly wanders or morphs. + +The first two do not imply the third. + +## Mumford-inspired direction + +Treat an image as containing coarse/geometric organization plus finer oscillatory +texture. Repeated roots can make the fine phase/color texture churn without causing +an equally strong displacement of coarse structure. + +For future acceptance work: + +- mask the immediate neighborhoods of zeros and poles when they would dominate a + measurement; +- construct a Gaussian pyramid (for example 1, 1/2, 1/4, 1/8, 1/16 resolution); +- emphasize gradients or other palette-insensitive coarse structure rather than raw + RGB at the coarser levels; +- divide coarse levels into reasonably large blocks and find where each block's + structure moved between two renderer frames; +- measure displacement magnitude, spatial coherence between neighboring blocks, and + how much a smooth geometric warp of frame A improves its match to frame B; +- require meaningful motion at more than one scale so that neither a single moving + edge nor high-frequency recoloring can satisfy the test by itself. + +A minimal first implementation should prefer deterministic block matching over a +large optical-flow or machine-learning dependency. The test should remain inspectable: +we should be able to say which coarse regions moved, by how far, and whether neighboring +regions moved coherently. + +## What not to do + +- Do not turn up the Cauchy amplitudes merely to make an RGB threshold pass. +- Do not use percent-changed-pixels as the definition of visible motion. +- Do not substitute a single Fourier/power-spectrum difference for geometry; texture + statistics and spatial organization are different questions. +- Do not delete the direct `q_t` test. It cleanly proves that the off-screen wanderers + themselves are moving even when the divisor hides or transforms their visual effect. + +## Current acceptance direction + +Keep three questions distinct: + +- **field motion**: direct change of `q_t` on sampled visible points; +- **pixel motion**: coarse smoke test that the renderer is not frozen; +- **structure motion**: multiscale coherent displacement of coarse image organization. + +When designing the third, go back to Mumford/Desolneux rather than inventing another +whole-frame scalar that can be fooled by texture churn. + +## Papers actually consulted for this work + +Keep this section distinct from a generic bibliography. These are papers or manuscripts +consulted while developing the structure-motion idea and its source trail. The Brown +copies are preferred because they are freely readable from Mumford's own archive. + +- David Mumford and Jayant Shah, **"Optimal Approximations by Piecewise Smooth + Functions and Associated Variational Problems,"** *Communications on Pure and + Applied Mathematics* 42 (1989), 577-685. + https://www.dam.brown.edu/people/mumford/vision/papers/1989c--Mumford-Shah-Wiley.pdf + Relevance here: coarse piecewise structure, edges, and the idea that an image can + have meaningful organization at a scale even when fine texture violates the model. + +- David Mumford, **"Mathematical Theories of Shape: Do They Model Perception?"** + SPIE *Geometric Methods in Computer Vision* 1570 (1991), 2-10. + https://www.dam.brown.edu/people/mumford/vision/papers/1991d--MathThShape-DAM.pdf + Relevance here: explicit multiscale analysis of visual signals and the problem of + defining similarity in terms of shape rather than raw samples. + +- David Mumford, **"Pattern Theory: A Unifying Perspective,"** first European + Congress of Mathematics (1994), revised in *Perception as Bayesian Inference* + (1996), 25-62. + https://www.dam.brown.edu/people/mumford/vision/papers/1994c-96--PattThUnifyingPersp-NC.pdf + Relevance here: natural variation as domain warping/deformation and pattern models + whose geometry is not captured by simple Gaussian or pointwise distances. + +- Song Chun Zhu and David Mumford, **"Learning Generic Prior Models for Visual + Computation,"** CVPR (1997), 463-469. + https://www.dam.brown.edu/people/mumford/vision/papers/1997a--LearningPriors-Zhu-IEEE.pdf + Relevance here: learn image statistics across scales rather than assume an arbitrary + smoothness model; scale invariance is treated as a property a useful prior should + respect. + +- Song Chun Zhu and David Mumford, **"Prior Learning and Gibbs Reaction-Diffusion,"** + *IEEE Transactions on Pattern Analysis and Machine Intelligence* 19(11) (1997), + 1236-1250. + https://www.dam.brown.edu/people/mumford/vision/papers/1997b--PriorLGibbsR-D-Zhu-IEEE.pdf + Relevance here: connects learned natural-image statistics, scale behavior, and + image-processing dynamics. + +- Song Chun Zhu, Yingnian Wu, and David Mumford, **"Filters, Random Fields and + Maximum Entropy (FRAME): Towards a Unified Theory for Texture Modeling,"** + *International Journal of Computer Vision* 27 (1998). + https://www.dam.brown.edu/people/mumford/vision/papers/1998b--Frame-ZhuWu-journal.pdf + Relevance here: texture can be characterized through distributions of filter + responses; texture statistics are useful but are not the same thing as large-scale + geometric motion. + +- Jinggang Huang and David Mumford, **"Statistics of Natural Images and Models,"** + CVPR (1999), 541-547. + https://www.dam.brown.edu/people/mumford/vision/papers/1999c--ImageStats-Huang-IEEE.pdf + Relevance here: direct empirical evidence for multiscale/near-scale-invariant image + statistics and Haar/wavelet response distributions. This is a strong source for + testing at several resolutions rather than only at native pixels. + +- David Mumford, **"Pattern Theory: The Mathematics of Perception,"** ICM 2002. + https://www.dam.brown.edu/ptg/REPORTS/02-10.pdf + Relevance here: concise mathematical statement of pattern theory as inference over + noisy, incomplete signals whose interesting structure repeats with variations and + clutter. + +- Ann B. Lee, Kim S. Pedersen, and David Mumford, **"The Nonlinear Statistics of + High-Contrast Patches in Natural Images,"** *International Journal of Computer + Vision* 54 (2003), 83-103. + https://www.dam.brown.edu/people/mumford/vision/papers/2003a--Stats-ALeePedersen-journal.pdf + Relevance here: local high-contrast patches concentrate near nonlinear low-dimensional + geometric structures; full local distributions contain information that marginal or + spectral summaries discard. + +- David Mumford, **"Empirical Statistics and Stochastic Models for Visual Signals,"** + in *Brain and Systems: New Directions in Statistical Signal Processing* (2006). + https://www.dam.brown.edu/people/mumford/vision/papers/2006d--SurveyStochModels-PrfShts.pdf + Relevance here: the broad survey tying together natural-image statistics, filters, + wavelets, local primitives, scale, and stochastic image models. This is the first + supporting paper to revisit when the current test needs a stronger statistical basis. + +### Mumford archive indexes + +These are useful discovery pages, not substitutes for citing the individual papers: + +- image statistics: https://www.dam.brown.edu/people/mumford/vision/stats.html +- pattern theory: https://www.dam.brown.edu/people/mumford/vision/pattern.html +- shape: https://www.dam.brown.edu/people/mumford/vision/shape.html +- segmentation/parsing: https://www.dam.brown.edu/people/mumford/vision/segment.html + +When adding future sources, say whether they were actually consulted for a test or are +only candidates for later reading. Do not inflate the source trail by listing every +reference in Mumford's papers. \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 0000000..9e804cb --- /dev/null +++ b/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +Holomorphic diff --git a/tests/check_cauchy_motion.py b/tests/check_cauchy_motion.py new file mode 100644 index 0000000..dd174aa --- /dev/null +++ b/tests/check_cauchy_motion.py @@ -0,0 +1,99 @@ +from __future__ import annotations + +import argparse +from pathlib import Path + +from PIL import Image, ImageChops, ImageDraw, ImageStat + + +DEFAULT_MIN_MEAN_ABS_RGB = 1.5 +DEFAULT_MIN_CHANGED_FRACTION = 0.10 +PIXEL_CHANGE_THRESHOLD = 8 + + +def exclusion_from_file(path: Path | None) -> tuple[int, int, int] | None: + if path is None: + return None + parts = path.read_text().split() + if len(parts) != 3: + raise SystemExit("exclusion file must contain: X Y RADIUS") + return tuple(int(part) for part in parts) + + +def measure_motion( + first_path: Path, + second_path: Path, + exclusion: tuple[int, int, int] | None = None, +) -> tuple[float, float]: + first = Image.open(first_path).convert("RGB") + second = Image.open(second_path).convert("RGB") + if first.size != second.size: + raise SystemExit("motion screenshots have different dimensions") + + width, height = first.size + box = ( + int(width * 0.10), + int(height * 0.15), + int(width * 0.90), + int(height * 0.85), + ) + diff = ImageChops.difference(first.crop(box), second.crop(box)) + mask = Image.new("L", diff.size, 255) + + if exclusion is not None: + center_x, center_y, radius = exclusion + center_x -= box[0] + center_y -= box[1] + draw = ImageDraw.Draw(mask) + draw.ellipse( + ( + center_x - radius, + center_y - radius, + center_x + radius, + center_y + radius, + ), + fill=0, + ) + + mean_abs_rgb = sum(ImageStat.Stat(diff, mask=mask).mean) / 3.0 + changed = 0 + eligible = 0 + for pixel, selected in zip(diff.getdata(), mask.getdata()): + if not selected: + continue + eligible += 1 + if max(pixel) >= PIXEL_CHANGE_THRESHOLD: + changed += 1 + changed_fraction = changed / max(eligible, 1) + return mean_abs_rgb, changed_fraction + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("first", type=Path) + parser.add_argument("second", type=Path) + parser.add_argument("--exclude-file", type=Path) + parser.add_argument("--min-mean", type=float, default=DEFAULT_MIN_MEAN_ABS_RGB) + parser.add_argument( + "--min-changed-fraction", + type=float, + default=DEFAULT_MIN_CHANGED_FRACTION, + ) + args = parser.parse_args() + + exclusion = exclusion_from_file(args.exclude_file) + mean_abs_rgb, changed_fraction = measure_motion( + args.first, + args.second, + exclusion, + ) + print( + f"Cauchy motion mean_abs_rgb={mean_abs_rgb:.3f} " + f"changed_fraction={changed_fraction:.3f}" + ) + if mean_abs_rgb < args.min_mean or changed_fraction < args.min_changed_fraction: + raise SystemExit("Cauchy-field motion is still too visually weak") + + +if __name__ == "__main__": + main() diff --git a/tests/check_cauchy_structure_motion.py b/tests/check_cauchy_structure_motion.py new file mode 100644 index 0000000..0fab927 --- /dev/null +++ b/tests/check_cauchy_structure_motion.py @@ -0,0 +1,208 @@ +from __future__ import annotations + +import argparse +import statistics +from pathlib import Path + +from PIL import Image, ImageChops, ImageDraw, ImageFilter, ImageStat + + +PYRAMID_SHORT_SIDES = (28, 22, 18) +DEFAULT_MIN_RAW_RGB = 1.0 +DEFAULT_MIN_COARSE_GRAY = 1.0 +DEFAULT_MIN_STRUCTURE_RETENTION = 0.30 + + +def exclusion_from_file(path: Path | None) -> tuple[int, int, int] | None: + if path is None: + return None + parts = path.read_text().split() + if len(parts) != 3: + raise SystemExit("exclusion file must contain: X Y RADIUS") + return tuple(int(part) for part in parts) + + +def analysis_box(width: int, height: int) -> tuple[int, int, int, int]: + return ( + int(width * 0.10), + int(height * 0.15), + int(width * 0.90), + int(height * 0.85), + ) + + +def full_resolution_mask( + size: tuple[int, int], + box: tuple[int, int, int, int], + exclusion: tuple[int, int, int] | None, +) -> Image.Image: + mask = Image.new("L", (box[2] - box[0], box[3] - box[1]), 255) + if exclusion is None: + return mask + + center_x, center_y, radius = exclusion + center_x -= box[0] + center_y -= box[1] + draw = ImageDraw.Draw(mask) + draw.ellipse( + ( + center_x - radius, + center_y - radius, + center_x + radius, + center_y + radius, + ), + fill=0, + ) + return mask + + +def raw_rgb_motion( + first: Image.Image, + second: Image.Image, + box: tuple[int, int, int, int], + exclusion: tuple[int, int, int] | None, +) -> float: + diff = ImageChops.difference(first.crop(box), second.crop(box)) + mask = full_resolution_mask(first.size, box, exclusion) + return sum(ImageStat.Stat(diff, mask=mask).mean) / 3.0 + + +def coarse_gray_image( + image: Image.Image, + box: tuple[int, int, int, int], + target_short_side: int, +) -> Image.Image: + crop = image.crop(box).convert("L") + short_side = min(crop.size) + source_pixels_per_coarse_pixel = short_side / target_short_side + + # Blur before decimation so fine phase/color bands do not alias into the + # coarse image. This is the scale-space part of the test: only structure + # surviving a substantial low-pass operation is allowed to count here. + crop = crop.filter( + ImageFilter.GaussianBlur(radius=source_pixels_per_coarse_pixel) + ) + + if crop.width <= crop.height: + width = target_short_side + height = max(1, round(crop.height * target_short_side / crop.width)) + else: + height = target_short_side + width = max(1, round(crop.width * target_short_side / crop.height)) + + return crop.resize((width, height), Image.Resampling.LANCZOS) + + +def coarse_mask( + coarse_size: tuple[int, int], + box: tuple[int, int, int, int], + exclusion: tuple[int, int, int] | None, +) -> Image.Image: + mask = Image.new("L", coarse_size, 255) + if exclusion is None: + return mask + + center_x, center_y, radius = exclusion + scale_x = coarse_size[0] / (box[2] - box[0]) + scale_y = coarse_size[1] / (box[3] - box[1]) + center_x = (center_x - box[0]) * scale_x + center_y = (center_y - box[1]) * scale_y + radius_x = radius * scale_x + radius_y = radius * scale_y + + draw = ImageDraw.Draw(mask) + draw.ellipse( + ( + center_x - radius_x, + center_y - radius_y, + center_x + radius_x, + center_y + radius_y, + ), + fill=0, + ) + return mask + + +def coarse_gray_motion( + first: Image.Image, + second: Image.Image, + box: tuple[int, int, int, int], + exclusion: tuple[int, int, int] | None, + target_short_side: int, +) -> float: + first_coarse = coarse_gray_image(first, box, target_short_side) + second_coarse = coarse_gray_image(second, box, target_short_side) + diff = ImageChops.difference(first_coarse, second_coarse) + mask = coarse_mask(diff.size, box, exclusion) + return ImageStat.Stat(diff, mask=mask).mean[0] + + +def measure_structure_motion( + first_path: Path, + second_path: Path, + exclusion: tuple[int, int, int] | None = None, +) -> tuple[float, list[float], list[float]]: + first = Image.open(first_path).convert("RGB") + second = Image.open(second_path).convert("RGB") + if first.size != second.size: + raise SystemExit("motion screenshots have different dimensions") + + box = analysis_box(*first.size) + raw = raw_rgb_motion(first, second, box, exclusion) + coarse = [ + coarse_gray_motion(first, second, box, exclusion, short_side) + for short_side in PYRAMID_SHORT_SIDES + ] + retention = [value / raw if raw > 1.0e-9 else 0.0 for value in coarse] + return raw, coarse, retention + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("first", type=Path) + parser.add_argument("second", type=Path) + parser.add_argument("--exclude-file", type=Path) + parser.add_argument("--min-raw-rgb", type=float, default=DEFAULT_MIN_RAW_RGB) + parser.add_argument( + "--min-coarse-gray", + type=float, + default=DEFAULT_MIN_COARSE_GRAY, + ) + parser.add_argument( + "--min-structure-retention", + type=float, + default=DEFAULT_MIN_STRUCTURE_RETENTION, + ) + args = parser.parse_args() + + exclusion = exclusion_from_file(args.exclude_file) + raw, coarse, retention = measure_structure_motion( + args.first, + args.second, + exclusion, + ) + median_coarse = statistics.median(coarse) + median_retention = statistics.median(retention) + + coarse_text = ",".join(f"{value:.3f}" for value in coarse) + retention_text = ",".join(f"{value:.3f}" for value in retention) + print( + f"Cauchy structure raw_rgb={raw:.3f} " + f"coarse_gray=[{coarse_text}] " + f"retention=[{retention_text}] " + f"median_retention={median_retention:.3f}" + ) + + if raw < args.min_raw_rgb: + raise SystemExit("running APK is too static for a structure-motion test") + if median_coarse < args.min_coarse_gray: + raise SystemExit("large-scale grayscale structure is too static") + if median_retention < args.min_structure_retention: + raise SystemExit( + "motion is dominated by fine color/texture churn rather than " + "large-scale structure" + ) + + +if __name__ == "__main__": + main() diff --git a/tests/seed_repeated_zeros.sh b/tests/seed_repeated_zeros.sh new file mode 100644 index 0000000..3459a1f --- /dev/null +++ b/tests/seed_repeated_zeros.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +set -euo pipefail + +log_file=${1:-holomorphic-emulator.log} +coordinate_file=${2:-/tmp/cauchy-repeated-zero-cluster} + +read -r width height < <( + sed -n 's/.*Cauchy field ready: surface=\([0-9][0-9]*\)x\([0-9][0-9]*\).*/\1 \2/p' \ + "$log_file" | tail -1 +) + +test -n "${width:-}" +test -n "${height:-}" + +min_side=$width +if (( height < width )); then + min_side=$height +fi + +pixel_radius=$((42 * min_side / 100)) +cluster_x=$((width / 2 - 34 * pixel_radius / 100)) +cluster_y=$((height / 2)) +exclude_radius=$((22 * min_side / 100)) + +# The app starts with one zero at z=-0.34. Leave it in place and add seven +# more within a few pixels. This models an order-eight repeated root without +# depending on a drag gesture merely to construct the runtime fixture. +for offset in \ + '2 0' \ + '-2 1' \ + '1 -2' \ + '-1 -2' \ + '3 2' \ + '-3 2' \ + '0 3' +do + read -r dx dy <<< "$offset" + adb shell input tap "$((cluster_x + dx))" "$((cluster_y + dy))" +done + +printf '%s %s %s\n' "$cluster_x" "$cluster_y" "$exclude_radius" > "$coordinate_file" diff --git a/tests/test_cauchy_field.py b/tests/test_cauchy_field.py new file mode 100644 index 0000000..d90c4de --- /dev/null +++ b/tests/test_cauchy_field.py @@ -0,0 +1,108 @@ +from __future__ import annotations + +import math +import unittest + + +SOURCE_COUNT = 24 +TAU = 2.0 * math.pi + + +def fract(value: float) -> float: + return value - math.floor(value) + + +def hash1(value: float) -> float: + return fract(math.sin(value * 127.1 + 31.7) * 43758.5453123) + + +def source_orbit_speed(index: int) -> float: + k = float(index) + return 0.11 + (0.19 - 0.11) * hash1(k + 4.37) + + +def source_handedness(index: int) -> float: + k = float(index) + return -1.0 if hash1(k + 7.91) < 0.5 else 1.0 + + +def source_position(index: int, time: float, view_radius: float) -> complex: + k = float(index) + initial_angle = TAU * hash1(k + 0.11) + angle = initial_angle + source_handedness(index) * source_orbit_speed(index) * time + + bend_phase = TAU * hash1(k + 11.23) + base = 1.8 * view_radius + radial = 0.35 * view_radius * math.sin(2.0 * angle + bend_phase) + radius = base + radial + + ellipticity = -0.08 + 0.16 * hash1(k + 14.67) + return radius * complex( + (1.0 + ellipticity) * math.cos(angle), + (1.0 - ellipticity) * math.sin(angle), + ) + + +def source_weight(index: int) -> complex: + k = float(index) + amplitude = 0.08 + (0.35 - 0.08) * hash1(k + 17.8) + phase = TAU * hash1(k + 21.4) + return amplitude * complex(math.cos(phase), math.sin(phase)) + + +def cauchy_field(z: complex, time: float, view_radius: float) -> complex: + return sum( + source_weight(index) / (source_position(index, time, view_radius) - z) + for index in range(SOURCE_COUNT) + ) + + +class CauchyFieldTests(unittest.TestCase): + def test_sources_stay_outside_the_visible_disk(self) -> None: + view_radius = 3.0 + for time in (0.0, 4.0, 17.0, 60.0, 120.0): + for index in range(SOURCE_COUNT): + with self.subTest(time=time, index=index): + self.assertGreater( + abs(source_position(index, time, view_radius)), + view_radius, + ) + + def test_residues_are_fixed_while_sources_move(self) -> None: + for index in range(SOURCE_COUNT): + with self.subTest(index=index): + self.assertEqual(source_weight(index), source_weight(index)) + self.assertNotEqual( + source_position(index, 0.0, 3.0), + source_position(index, 6.0, 3.0), + ) + + def test_field_motion_is_independent_of_the_divisor(self) -> None: + # Evaluate q_t directly. There are intentionally no zero/pole inputs, + # so repeated roots in R cannot manufacture or hide this signal. + width = 1080.0 + height = 2400.0 + pixel_radius = 0.42 * min(width, height) + view_radius = math.hypot(0.5 * width, 0.5 * height) / pixel_radius + sample_points = [ + complex(x * 0.30, y * 0.60) + for y in range(-3, 4) + for x in range(-3, 4) + ] + sample_points = [point for point in sample_points if abs(point) <= view_radius] + + for start_time in (0.0, 4.0, 17.0, 60.0): + deltas = [ + abs( + cauchy_field(point, start_time + 6.0, view_radius) + - cauchy_field(point, start_time, view_radius) + ) + for point in sample_points + ] + mean_delta = sum(deltas) / len(deltas) + with self.subTest(start_time=start_time): + self.assertGreater(mean_delta, 0.04) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_holomorphic_walk.c b/tests/test_holomorphic_walk.c deleted file mode 100644 index cab323a..0000000 --- a/tests/test_holomorphic_walk.c +++ /dev/null @@ -1,65 +0,0 @@ -#define _POSIX_C_SOURCE 200809L - -#include "holomorphic_walk.h" - -#include -#include -#include - -static void sleep_milliseconds(long milliseconds) { - struct timespec delay = { - .tv_sec = milliseconds / 1000, - .tv_nsec = (milliseconds % 1000) * 1000000L, - }; - nanosleep(&delay, NULL); -} - -int main(void) { - _Static_assert(HOLOMORPHIC_WALK_WORKER_COUNT == 3, "keep three search workers for the current CPU prototype"); - - float coefficients[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2] = {{0.0f, 0.0f}}; - float direction[HOLOMORPHIC_WALK_COEFFICIENT_COUNT][2]; - float score = INFINITY; - - if (!holomorphic_walk_start()) { - fputs("holomorphic_walk_start failed\n", stderr); - return 1; - } - - holomorphic_walk_publish(coefficients); - - bool found = false; - for (int attempt = 0; attempt < 200; ++attempt) { - if (holomorphic_walk_best_direction(direction, &score)) { - found = true; - break; - } - sleep_milliseconds(5); - } - - holomorphic_walk_stop(); - - if (!found) { - fputs("workers never produced a direction\n", stderr); - return 1; - } - if (!isfinite(score)) { - fputs("worker score is not finite\n", stderr); - return 1; - } - - float norm = holomorphic_walk_coefficient_budget(direction); - if (!isfinite(norm) || fabsf(norm - 1.0f) > 1.0e-3f) { - fprintf(stderr, "worker direction is not normalized: %.9g\n", norm); - return 1; - } - - if (holomorphic_walk_coefficient_budget(coefficients) > HOLOMORPHIC_WALK_COEFFICIENT_BUDGET) { - fputs("zero coefficient state somehow exceeds the budget\n", stderr); - return 1; - } - - printf("holomorphic workers ready: count=%d score=%.9g norm=%.9g\n", - HOLOMORPHIC_WALK_WORKER_COUNT, score, norm); - return 0; -} diff --git a/tests/test_renderer_boundary.py b/tests/test_renderer_boundary.py index cf44c66..4659cf5 100644 --- a/tests/test_renderer_boundary.py +++ b/tests/test_renderer_boundary.py @@ -25,6 +25,7 @@ def test_active_workflows_do_not_invoke_retired_renderer(self) -> None: self.assertNotIn("test_factor_state.c", workflows) self.assertNotIn("test_factor_snap.c", workflows) self.assertNotIn("test_gesture_state.c", workflows) + self.assertNotIn("cc -std=c11 -wall -wextra -werror \\\n -iandroid/app/src/main/cpp \\\n tests/test_holomorphic_walk.c", workflows) def test_dead_wegert_interface_copies_are_absent(self) -> None: cpp = ROOT / "android" / "app" / "src" / "main" / "cpp" @@ -34,10 +35,63 @@ def test_dead_wegert_interface_copies_are_absent(self) -> None: "factor_state.h", "gesture_state.h", "polynomial_overlay.h", + "holomorphic_walk.c", + "holomorphic_walk.h", ): with self.subTest(name=name): self.assertFalse((cpp / name).exists()) + def test_cauchy_field_is_gpu_driven(self) -> None: + cpp = ( + ROOT + / "android" + / "app" + / "src" + / "main" + / "cpp" + / "analytic_continuation_random.c" + ).read_text() + shader = ( + ROOT + / "android" + / "app" + / "src" + / "main" + / "assets" + / "continuation.frag.in" + ).read_text() + cmake = ( + ROOT / "android" / "app" / "src" / "main" / "cpp" / "CMakeLists.txt" + ).read_text() + + self.assertIn('glGetUniformLocation(engine->program, "u_time")', cpp) + self.assertIn("glUniform1f(engine->time_location, animation_time)", cpp) + self.assertNotIn("holomorphic_walk", cpp) + self.assertNotIn("holomorphic_walk", cmake) + self.assertNotIn("u_holomorphic_coefficients", shader) + + self.assertIn("#define SOURCE_COUNT 24", shader) + self.assertIn("uniform float u_time;", shader) + self.assertIn("vec2 source_position", shader) + self.assertIn("vec2 source_weight", shader) + self.assertIn("vec2 holomorphic_field", shader) + self.assertIn("1.8 * view_radius", shader) + self.assertIn("0.35 * view_radius", shader) + self.assertGreater(1.8 - 0.35, 1.0) + + def test_zoom_range_is_not_artificially_tight(self) -> None: + cpp = ( + ROOT + / "android" + / "app" + / "src" + / "main" + / "cpp" + / "analytic_continuation_random.c" + ).read_text() + self.assertIn("if (zoom < 0.1f) zoom = 0.1f;", cpp) + self.assertIn("if (zoom > 32.0f) zoom = 32.0f;", cpp) + def test_android_launches_only_the_native_explorer(self) -> None: manifest = ( ROOT / "android" / "app" / "src" / "main" / "AndroidManifest.xml" diff --git a/tests/test_wegert_color_parity.py b/tests/test_wegert_color_parity.py index f903ff9..53c5498 100644 --- a/tests/test_wegert_color_parity.py +++ b/tests/test_wegert_color_parity.py @@ -140,7 +140,7 @@ def test_meromorphic_and_holomorphic_value_is_complete_before_color_oracle(self) "log_modulus += 0.5 * log(radius_squared);", "phase -= atan(delta.y, delta.x);", "log_modulus -= 0.5 * log(radius_squared);", - "vec2 q = holomorphic_q(z);", + "vec2 q = holomorphic_field(z, u_time, view_radius);", "log_modulus += q.x;", "phase += q.y;", )