File:Black Hole Shadow.gif

Original file(1,280 × 720 pixels, file size: 3.09 MB, MIME type: image/gif, looped, 108 frames, 6.5 s)

Summary

Description
English: A plane wave of light passes by a static black hole and some of the light-rays are absorbed. The ones absorbed are those that have an impact parameter less than the radius . By the reversibility of light, this means that looking at this sphere is equivalent to looking at the surface of the event horizon. This lensing effect causes the apparent size of the black hole to be bigger, by a factor of about . The photon sphere is also depicted, as the region where orbiting light-rays going in are inevitably absorbed, and orbiting rays going out can escape.
Date
Source Own work
Author Hugo Spinelli
Source code
InfoField
Processing.py 3 code
import math  WIDTH, HEIGHT = 1920*2//3, 1080*2//3  fps = 60 c = 400.0 GM = 50*c**2 n_balls = 20000//1 too_close_factor = 1.00001 line_width = 100.0/n_balls #1.5 steps_per_frame = 1 refresh_dt = 0.05  save_frames = True filepath = 'frames\\'  hide_circles = True hide_absorbed = True absorbed_color = color(25,0,55,1000000/n_balls) light_color = color(255,255,0,1000000/n_balls) #color(255,230,0,10) fill_color = color(0) #color(0) or light_color background_color = color(100)  p0 = (WIDTH/2.0, HEIGHT/2.0) x_sep = 0.0  rs = 2*GM/c**2  def norm(v):     return sqrt(v[0]**2 + v[1]**2) def add(v1, v2):     return (v1[0]+v2[0], v1[1]+v2[1]) def subtract(v1, v2):     return (v1[0]-v2[0], v1[1]-v2[1]) def distance(v1, v2):     return norm(subtract(v1, v2)) def inner(v1, v2):     return v1[0]*v2[0] + v1[1]*v2[1] def product(v, a):     return (a*v[0], a*v[1]) def proj(v1, v2):     return product(v2, inner(v1,v2)/(norm(v2)**2)) def unitary(v):     return product(v, 1.0/norm(v)) def angle(v1, v2):     return math.atan2(v1[0]*v2[1]-v1[1]*v2[0],                   v1[0]*v2[0]+v1[1]*v2[1]) def rotate(v, theta):     s, c = math.sin(theta), math.cos(theta)     x, y = v[0], v[1]     return (c*x-s*y, s*x+c*y)      def sign(x):     if x<0:         return -1     if x>0:         return 1     return 0  def vround(v):     return (int(round(v[0])), int(round(v[1])))  def wrapToPi(x):     if x > math.pi:         return x-2*math.pi     if x < -math.pi:         return x+2*math.pi     return x  class Ball:     def __init__(self, p, v, radius):         self.p = p         self.v = v         self.phi_step = 0.001         self.vu = self.get_vu(0.001)                  self.radius = max(radius, 0.0)                  self.path = [self.p, self.p]          self.fill_color = fill_color         self.line_color = light_color          self.inactive = False         self.absorbed = False                  self.ellapsed_time = 0      def draw_circle(self):         if hide_circles or self.inactive:             return         fill(self.fill_color)         ellipse(self.p[0], self.p[1], 2*self.radius, 2*self.radius)              def get_phi(self, p):         return angle((1.0,0), subtract(p, p0))              def get_vu(self, dt):         dp = product(self.v, dt)         p_next = add(self.p, dp)                  du = 1/distance(p_next, p0) - 1/distance(self.p, p0)         dphi = self.get_phi(p_next) - self.get_phi(self.p)         return du/dphi              def update(self, dt):         if self.inactive:             return                  self.ellapsed_time += dt                  direction = sign(angle(subtract(self.p, p0), self.v))                  dphi = direction*self.phi_step         u = 1/distance(self.p, p0)         phi = self.get_phi(self.p)         u_next = u + self.vu*dphi         phi_next = phi + dphi         p_next = (cos(phi_next)/u_next, sin(phi_next)/u_next)         p_next = add(p_next, p0)                  self.v = subtract(p_next, self.p)         dp = self.get_dp(dt)         p_prev = self.p         self.p = add(self.p, dp)         self.v = product(dp, 1.0/dt)                  dphi = wrapToPi(self.get_phi(self.p) - self.get_phi(p_prev))         dvu = (1.5*rs*u**2-u)*dphi         self.vu += dvu          if (self.ellapsed_time + dt)//refresh_dt > self.ellapsed_time//refresh_dt:             self.path.append(self.p)         else:             self.path[-1] = self.p              def get_dp(self, dt):         # v only gives the local orbit direction         r_vec = subtract(self.p, p0)         x, y = r_vec         r = norm(r_vec)         phi = self.get_phi(self.p)                  k = self.v[1]/self.v[0]         b = 1-rs/r                  #https://bit.ly/2WaCIcB         k1 = sqrt(  -1/( 2*(b-1)*k*x*y - (b*x**2+y**2)*k**2 - b*y**2 - x**2 )  )                  dx = sign(self.v[0])*abs(b*c*dt*r*k1)         dy = sign(self.v[1])*abs(k*dx)                  return (dx, dy)              def is_too_close(self):         if norm(subtract(self.p, p0)) < too_close_factor*(2*GM/c**2):             return True              def set_too_close(self):         self.inactive = True         self.absorbed = True         self.line_color = absorbed_color      def is_out_of_bounds(self):         dx = 4*abs(self.v[0])*refresh_dt         dy = 4*abs(self.v[1])*refresh_dt         if (self.p[0]<-dx       and self.v[0]<-1) or \            (self.p[0]>WIDTH+dx  and self.v[0]> 1) or \            (self.p[1]<-dy       and self.v[1]<-1) or \            (self.p[1]>HEIGHT+dy and self.v[1]> 1):             return True         return False          def set_out_of_bounds(self):         self.inactive = True  balls = [] M = 2.0 radius = M*HEIGHT/(2.0*n_balls) for ky in range(n_balls/2):     x = WIDTH + radius + x_sep     y = radius + ky*2*radius     balls += [         Ball((x, HEIGHT/2.0+y), (-c, 0), radius),         Ball((x, HEIGHT/2.0-y), (-c, 0), radius)     ] ordered_balls = list(range(n_balls)) for k in range(n_balls/2):     ordered_balls[n_balls/2+k] = balls[2*k]     ordered_balls[n_balls/2-k-1] = balls[2*k+1] #balls.append(Ball((0, 1.51*2*GM/c**2), (-c, 0), radius)) #balls.append(Ball((0, 1.49*2*GM/c**2), (-c, 0), radius))  def draw_wave_front(inactive_balls):     active_ratio = (n_balls-inactive_balls)/(1.0*n_balls)     cuttoff = 0.1     if active_ratio < cuttoff:         return          stroke(color(255,255,0,255.0*(active_ratio-cuttoff)))     strokeWeight(1.5)     noFill()          beginShape()     started = False     for k in range(n_balls/2+1):         ball = ordered_balls[n_balls/2-k]         p = ball.p         if ball.absorbed:             continue         if distance(p, p0) < rs + 0.5:             continue         if not started:             started = True             x, y = p             curveVertex(x, y)             curveVertex(x, y)             continue         if distance((x,y), p) < 10:             continue         x, y = p         curveVertex(x, y)     x, y = p     curveVertex(x, y)     curveVertex(x, y)     endShape()          beginShape()     started = False     for k in range(1,n_balls/2+1):         ball = ordered_balls[k-n_balls/2-1]         p = ball.p         if ball.absorbed:             continue         if distance(p, p0) < rs + 0.5:             continue         if not started:             started = True             x, y = p             curveVertex(x, y)             curveVertex(x, y)             continue         if distance((x,y), p) < 10:             continue         x, y = p         curveVertex(x, y)     x, y = p     curveVertex(x, y)     curveVertex(x, y)     endShape()              stroke(0)     strokeWeight(1)  def draw_solid_path():     def draw_solid_path_for(ball1, ball2):         if ball1.absorbed or ball2.absorbed:             return                  def get_xy(ball, k2):             if k2 < len(ball.path):                 return ball.path[k2]             return ball.path[-1]                  d1 = distance(ball1.p, p0)         d2 = distance(ball2.p, p0)         closeness = 1.0         cuttoff = 1.5         if d1 < cuttoff*rs or d2 < cuttoff*rs:             x = min(d1, d2)/rs             closeness = sqrt((x-1.0)/(cuttoff-1.0))          noStroke()         for k2 in range(min(len(ball1.path), len(ball2.path)) -1):             x1, y1 = get_xy(ball1, k2)             x2, y2 = get_xy(ball1, k2+1)             x3, y3 = get_xy(ball2, k2+1)             x4, y4 = get_xy(ball2, k2)             d = distance((x2, y2), (x3, y3))             a = closeness*(255.0*ball1.radius)/d             a = max(0,min(a,255))             fill(color(255,255,0,a))             quad(x1, y1, x2, y2, x3, y3, x4, y4)         stroke(0)      for k in range(n_balls/2):         ball1 = ordered_balls[n_balls/2-k]         ball2 = ordered_balls[n_balls/2-k-1]         draw_solid_path_for(ball1, ball2)     for k in range(1,n_balls/2):         ball1 = ordered_balls[k-n_balls/2-1]         ball2 = ordered_balls[k-n_balls/2]         draw_solid_path_for(ball1, ball2)  img_shadow = None img_photon_sphere = None img_event_horizon = None def setup():     global img_shadow, img_photon_sphere, img_event_horizon     size(WIDTH, HEIGHT)     frameRate(fps)     smooth(8)     img_shadow = loadImage('Shadow.png')     img_photon_sphere = loadImage('Photon Sphere.png')     img_event_horizon = loadImage('Event Horizon.png')     s = 0.3     img_shadow.resize(int(round(s*img_shadow.width)), 0)     img_photon_sphere.resize(int(round(s*img_photon_sphere.width)), 0)     img_event_horizon.resize(int(round(s*img_event_horizon.width)), 0)  frame = 1 post_frame = 0 finished = False def draw():     global frame, post_frame, finished     if finished:         return     background(background_color)          dt = 1.0/fps      inactive_balls = 0     for ball in balls:         if ball.inactive:             inactive_balls += 1             continue         if ball.is_out_of_bounds():             ball.set_out_of_bounds()             continue         if ball.is_too_close():             ball.set_too_close()             continue      if balls[0].p[0] > WIDTH + balls[0].radius:         print balls[0].p[0] - WIDTH         fill(color(0, 0, 0))         ellipse(p0[0], p0[1], 2*rs, 2*rs)                  for ball in balls:             for k in range(steps_per_frame):                 ball.update(dt/steps_per_frame)         return      for ball in balls:         ball.draw_circle()              draw_wave_front(inactive_balls)     draw_solid_path()            fill(color(0, 0, 0))     ellipse(p0[0], p0[1], 2*rs, 2*rs)          if inactive_balls > 0.96*n_balls:         post_frame += 1     if True:         a = 255  # post_frame*255.0/30                  M = sqrt(27)/2         noFill()         strokeWeight(2.0)         stroke(color(0, 0, 100, a))         line(p0[0],p0[1]-M*rs, p0[0]+WIDTH-p0[0], p0[1]-M*rs)         line(p0[0],p0[1]-M*rs+2*M*rs, p0[0]+WIDTH-p0[0], p0[1]-M*rs+2*M*rs)         ellipse(p0[0], p0[1], M*2*rs, M*2*rs)         stroke(color(255,255,0,a))         ellipse(p0[0], p0[1], 1.5*2*rs, 1.5*2*rs)         strokeWeight(1.0)                  font = createFont('Georgia', 32)         textFont(font)                  fill(color(0, 0, 100, a))         s = 'Shadow:'         text(s, p0[0], p0[1]-M*rs-2-22)         tint(255, a)         image(img_shadow, p0[0]+textWidth(s)+10, p0[1]-M*rs-2-74)                  fill(color(255,255,0,a))         s = 'Photon Sphere:'         buff = 0.1         text(s, p0[0], p0[1]-(1.5 + buff)*rs-2-5)         tint(255, a)         image(img_photon_sphere, p0[0]+textWidth(s)+10, p0[1]-(1.5 + buff)*rs-2-26)                  fill(color(0,0,0,a))         s = 'Event Horizon:'         text(s, p0[0], p0[1]-rs-2)         tint(255, a)         image(img_event_horizon, p0[0]+textWidth(s)+10, p0[1]-rs-2-67+22)                  fill(color(255,255,255,a))         stroke(color(255,255,255,a))         text('Singularity', p0[0],p0[1]-5)         ellipse(p0[0], p0[1], 3, 3)                  stroke(0)      for ball in balls:         for k in range(steps_per_frame):             ball.update(dt/steps_per_frame)          if save_frames and post_frame<180:         saveFrame(filepath + str(frame).zfill(4) + '.png')     if post_frame<180:         print frame     if post_frame==180:         print 'Done!'         finished = True          frame += 1 

The PNG files can be generated, e.g., from https://latex.codecogs.com/eqneditor/editor.php. The LaTeX code for each file is:
LaTeX code
% "Shadow.png": \color[RGB]{0,0,100}\boldsymbol{R = \frac{\sqrt{27}}{2}r_s \approx 2.6r_s}  % "Photon Sphere.png": \color{yellow}\boldsymbol{r = 1.5r_s}  % "Event Horizon.png": \boldsymbol{r_s = \frac{2GM}{c^2}} 

Licensing

I, the copyright holder of this work, hereby publish it under the following license:
Creative Commons CC-Zero This file is made available under the Creative Commons CC0 1.0 Universal Public Domain Dedication.
The person who associated a work with this deed has dedicated the work to the public domain by waiving all of their rights to the work worldwide under copyright law, including all related and neighboring rights, to the extent allowed by law. You can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.

Captions

Animated diagram showing the event horizon, the photon sphere, and the shadow of a black hole.

27 September 2023

image/gif

dd2fee407ab4d247c6ae30521389dea7a51caf09

3,237,835 byte

6.479999999999987 second

720 pixel

1,280 pixel

File history

Click on a date/time to view the file as it appeared at that time.

Date/TimeThumbnailDimensionsUserComment
current03:58, 27 September 2023Thumbnail for version as of 03:58, 27 September 20231,280 × 720 (3.09 MB)Hugo SpinelliUploaded own work with UploadWizard
The following pages on the English Wikipedia use this file (pages on other projects are not listed):

Global file usage

The following other wikis use this file:

Metadata