def test07_rotate_stokes_basis(variant_scalar_rgb): from mitsuba.core import Transform4f from mitsuba.render.mueller import stokes_basis, rotate_stokes_basis # Each Stokes vector describes light polarization only w.r.t. a certain reference # frame determined by unit vector perpendicular to the direction of travel. # # Here we test a duality between rotating the polarization direction (e.g. from # linear polarization at 0˚ to 90˚) and rotating the Stokes reference frame # in the opposite direction. # We start out with horizontally polarized light (Stokes vector [1, 1, 0, 0]) # and perform several transformations. w = [0, 0, 1] # Optical axis / forward direction s_00 = [1, 1, 0, 0] # Horizontally polarized light b_00 = stokes_basis(w) # Corresponding Stokes basis def rotate_vector(v, axis, angle): return Transform4f.rotate(axis, angle).transform_vector(v) # Switch to basis rotated by 90˚. b_90 = rotate_vector(b_00, w, 90.0) assert ek.allclose(b_90, [0, 1, 0], atol=1e-3) # Now polarization should be at 90˚ / vertical (with Stokes vector [1, -1, 0, 0]) s_90 = rotate_stokes_basis(w, b_00, b_90) @ s_00 assert ek.allclose(s_90, [1.0, -1.0, 0.0, 0.0], atol=1e-3) # Switch to basis rotated by +45˚. b_45 = rotate_vector(b_00, w, +45.0) assert ek.allclose(b_45, [0.70712, 0.70712, 0.0], atol=1e-3) # Now polarization should be at -45˚ (with Stokes vector [1, 0, -1, 0]) s_45 = rotate_stokes_basis(w, b_00, b_45) @ s_00 assert ek.allclose(s_45, [1.0, 0.0, -1.0, 0.0], atol=1e-3) # Switch to basis rotated by -45˚. b_45_neg = rotate_vector(b_00, w, -45.0) assert ek.allclose(b_45_neg, [0.70712, -0.70712, 0.0], atol=1e-3) # Now polarization should be at +45˚ (with Stokes vector [1, 0, +1, 0]) s_45_neg = rotate_stokes_basis(w, b_00, b_45_neg) @ s_00 assert ek.allclose(s_45_neg, [1.0, 0.0, +1.0, 0.0], atol=1e-3)
def test08_rotate_mueller_basis(variant_scalar_rgb): from mitsuba.core import Transform4f from mitsuba.render.mueller import stokes_basis, rotated_element, rotate_mueller_basis, \ rotate_mueller_basis_collinear, linear_polarizer # If we have an optical element such as a linear polarizer, its rotation around # the optical axis can also be interpreted as a change of both incident and # outgoing Stokes reference frames: w = [0, 0, 1] # Optical axis / forward direction s_00 = [1, 1, 0, 0] # Horizontally polarized light b_00 = stokes_basis(w) # Corresponding Stokes basis M = linear_polarizer() def rotate_vector(v, axis, angle): return Transform4f.rotate(axis, angle).transform_vector(v) # As reference, rotate the element directly by -45˚ M_rotated_element = rotated_element(-45 * ek.pi/180, M) # Now rotate the two reference bases by 45˚ instead and make sure results are # equivalent. # More thorough explanation of what's going on here: # Mueller matrix 'M' works in b_00 = [1, 0, 0], and we want to construct # another matrix that works in b_45 =[0.707, 0.707, 0] instead. # 'rotate_mueller_basis' does exactly that: # R(b_00 -> b_45) * M * R(b_45 -> b_00) = R(+45˚) * M * R(-45˚) which is # equivalent to rotating the elemnt by -45˚. b_45 = rotate_vector(b_00, w, +45.0) M_rotated_bases = rotate_mueller_basis(M, w, b_00, b_45, w, b_00, b_45) assert ek.allclose(M_rotated_element, M_rotated_bases, atol=1e-5) # Also test alternative rotation method that assumes collinear incident and # outgoing directions. M_rotated_bases_aligned = rotate_mueller_basis_collinear(M, w, b_00, b_45) assert ek.allclose(M_rotated_element, M_rotated_bases_aligned, atol=1e-5)
def test07_path_tracer_half_wave(variant_scalar_mono_polarized): from mitsuba.core import Spectrum from mitsuba.core.xml import load_string from mitsuba.render import BSDFContext, TransportMode from mitsuba.render.mueller import stokes_basis, rotate_stokes_basis_m def spectrum_from_stokes(v): res = Spectrum(0.0) for i in range(4): res[i, 0] = v[i] return res # Another test involving a half-wave plate, this time inside an actual # polarized path tracer. # (Serves also as test case for the polarized path tracer itself.) # # Following "Polarized Light - Fundamentals and Applications" by Edward Collett # Chapter 5.3: # # Case 1 & 2) Switch between diagonal linear polarization states (-45˚ & + 45˚) # # In this test, a polarizer and half-wave plate are placed between light # source and camera. # The polarizer is used to convert emitted light into a +-45˚ linearly # polarized state. The subsequent retarder will flip the polarization # axis to the opposite one. linear_pos = spectrum_from_stokes([1, 0, +1, 0]) linear_neg = spectrum_from_stokes([1, 0, -1, 0]) angles = [+45.0, -45.0] expected = [linear_neg, linear_pos] observed = [] for angle in angles: scene_str = """<scene version='2.0.0'> <integrator type="path"/> <sensor type="perspective"> <float name="fov" value="0.00001"/> <transform name="to_world"> <lookat origin="0, 10, 0" target="0, 0, 0" up ="0, 0, 1"/> </transform> <film type="hdrfilm"> <integer name="width" value="1"/> <integer name="height" value="1"/> <rfilter type="gaussian"/> <string name="pixel_format" value="rgb"/> </film> </sensor> <!-- Light source --> <shape type="rectangle"> <transform name="to_world"> <rotate x="1" y="0" z="0" angle="-90"/> <translate y="-10"/> </transform> <emitter type="area"> <spectrum name="radiance" value="1"/> </emitter> </shape> <!-- Polarizer at given angle --> <shape type="rectangle"> <bsdf type="polarizer"> </bsdf> <transform name="to_world"> <rotate x="1" y="0" z="0" angle="-90"/> <rotate y="1" angle="{}"/> <!-- Rotate around optical axis --> <translate y="-5"/> </transform> </shape> <!-- Half-wave plate. --> <shape type="rectangle"> <bsdf type="retarder"> <spectrum name="delta" value="180"/> </bsdf> <transform name="to_world"> <rotate x="1" y="0" z="0" angle="-90"/> <translate y="0"/> </transform> </shape> </scene>""".format(angle) scene = load_string(scene_str) integrator = scene.integrator() sensor = scene.sensors()[0] sampler = sensor.sampler() # Sample ray from sensor ray, _ = sensor.sample_ray_differential(0.0, 0.5, [0.5, 0.5], [0.5, 0.5]) # Call integrator value, _, _ = integrator.sample(scene, sampler, ray) # Normalize Stokes vector value /= value[0, 0] # Align output stokes vector (based on ray.d) with optical table. (In this configuration, this is a no-op.) forward = -ray.d basis_cur = stokes_basis(forward) basis_tar = [-1, 0, 0] R = rotate_stokes_basis_m(forward, basis_cur, basis_tar) value = R @ value observed.append(value) # Make sure observations match expectations for k in range(len(expected)): assert ek.allclose(observed[k], expected[k], atol=1e-3)
def test05_sample_half_wave_world(variant_scalar_mono_polarized): from mitsuba.core import Ray3f, Spectrum from mitsuba.core.xml import load_string from mitsuba.render import BSDFContext, TransportMode from mitsuba.render.mueller import stokes_basis, rotate_mueller_basis_collinear def spectrum_from_stokes(v): res = Spectrum(0.0) for i in range(4): res[i, 0] = v[i] return res # Test polarized implementation. Special case of delta = 180˚, also known # as a half-wave plate. (In world coordinates.) # # Following "Polarized Light - Fundamentals and Applications" by Edward Collett # Chapter 5.3: # # Case 1 & 2) Switch between diagonal linear polarization states (-45˚ & + 45˚) # Case 3 & 4) Switch circular polarization direction linear_pos = spectrum_from_stokes([1, 0, +1, 0]) linear_neg = spectrum_from_stokes([1, 0, -1, 0]) circular_right = spectrum_from_stokes([1, 0, 0, +1]) circular_left = spectrum_from_stokes([1, 0, 0, -1]) # Incident direction forward = [0, -1, 0] ctx = BSDFContext() ctx.mode = TransportMode.Importance ray = Ray3f([0, 100, 0], forward, 0.0, 0.0) # Build scene with given polarizer rotation angle scene_str = """<scene version='2.0.0'> <shape type="rectangle"> <bsdf type="retarder"> <spectrum name="delta" value="180"/> </bsdf> <transform name="to_world"> <rotate x="1" y="0" z="0" angle="-90"/> <!-- Rotate s.t. it is no longer aligned with local coordinates --> </transform> </shape> </scene>""" scene = load_string(scene_str) # Intersect ray against geometry si = scene.ray_intersect(ray) bs, M_local = si.bsdf().sample(ctx, si, 0.0, [0.0, 0.0]) M_world = si.to_world_mueller(M_local, -si.wi, bs.wo) # Make sure we are measuring w.r.t. to the optical table M = rotate_mueller_basis_collinear(M_world, forward, stokes_basis(forward), [-1, 0, 0]) # Case 1) assert ek.allclose(M @ linear_pos, linear_neg, atol=1e-3) # Case 2) assert ek.allclose(M @ linear_neg, linear_pos, atol=1e-3) # Case 3) assert ek.allclose(M @ circular_right, circular_left, atol=1e-3) # Case 4) assert ek.allclose(M @ circular_left, circular_right, atol=1e-3)
def test03_sample_world(variant_scalar_mono_polarized): from mitsuba.core import Ray3f, Spectrum from mitsuba.core.xml import load_string from mitsuba.render import BSDFContext, TransportMode from mitsuba.render.mueller import stokes_basis, rotate_mueller_basis_collinear def spectrum_from_stokes(v): res = Spectrum(0.0) for i in range(4): res[i,0] = v[i] return res # Test polarized implementation, version in world coordinates. This involves # additional coordinate changes to the local reference frame and back. # # The polarizer is rotated to different angles and hit with fully # unpolarized light (Stokes vector [1, 0, 0, 0]). # We then test if the outgoing Stokes vector corresponds to the expected # rotation of linearly polarized light. # Incident direction forward = [0, 1, 0] stokes_in = spectrum_from_stokes([1, 0, 0, 0]) ctx = BSDFContext() ctx.mode = TransportMode.Importance ray = Ray3f([0, -100, 0], forward, 0.0, 0.0) # Polarizer rotation angles angles = [0, 90, +45, -45] # Expected outgoing Stokes vector expected_states = [spectrum_from_stokes([0.5, 0.5, 0, 0]), spectrum_from_stokes([0.5, -0.5, 0, 0]), spectrum_from_stokes([0.5, 0, +0.5, 0]), spectrum_from_stokes([0.5, 0, -0.5, 0])] for k in range(len(angles)): angle = angles[k] expected = expected_states[k] # Build scene with given polarizer rotation angle scene_str = """<scene version='2.0.0'> <shape type="rectangle"> <bsdf type="polarizer"/> <transform name="to_world"> <rotate x="0" y="0" z="1" angle="{}"/> <!-- Rotate around optical axis --> <rotate x="1" y="0" z="0" angle="-90"/> <!-- Rotate s.t. it is no longer aligned with local coordinates --> </transform> </shape> </scene>""".format(angle) scene = load_string(scene_str) # Intersect ray against geometry si = scene.ray_intersect(ray) bs, M_local = si.bsdf().sample(ctx, si, 0.0, [0.0, 0.0]) M_world = si.to_world_mueller(M_local, -si.wi, bs.wo) # Make sure we are measuring w.r.t. to the optical table M = rotate_mueller_basis_collinear(M_world, forward, stokes_basis(forward), [-1, 0, 0]) stokes_out = M @ stokes_in assert ek.allclose(stokes_out, expected, atol=1e-3)
def test04_path_tracer_polarizer(variant_scalar_mono_polarized): from mitsuba.core import Spectrum from mitsuba.core.xml import load_string from mitsuba.render import BSDFContext, TransportMode from mitsuba.render.mueller import stokes_basis, rotate_stokes_basis_m def spectrum_from_stokes(v): res = Spectrum(0.0) for i in range(4): res[i,0] = v[i] return res # Test polarizer implementation inside of an actual polarized path tracer. # (Serves also as test case for the polarized path tracer itself.) # # The polarizer is place in front of a light source that emits unpolarized # light (Stokes vector [1, 0, 0, 0]). The filter is rotated to different # angles and the transmitted polarization state is direclty observed with # a sensor. # We then test if the outgoing Stokes vector corresponds to the expected # rotation of linearly polarized light. horizontal_pol = spectrum_from_stokes([1, +1, 0, 0]) vertical_pol = spectrum_from_stokes([1, -1, 0, 0]) pos_diagonal_pol = spectrum_from_stokes([1, 0, +1, 0]) neg_diagonal_pol = spectrum_from_stokes([1, 0, -1, 0]) angles = [0, 90, +45, -45] expected = [horizontal_pol, vertical_pol, pos_diagonal_pol, neg_diagonal_pol] for k, angle in enumerate(angles): scene_str = """<scene version='2.0.0'> <integrator type="path"/> <sensor type="perspective"> <float name="fov" value="0.00001"/> <transform name="to_world"> <lookat origin="0, 10, 0" target="0, 0, 0" up ="0, 0, 1"/> </transform> <film type="hdrfilm"> <integer name="width" value="1"/> <integer name="height" value="1"/> <rfilter type="gaussian"/> <string name="pixel_format" value="rgb"/> </film> </sensor> <!-- Light source --> <shape type="rectangle"> <transform name="to_world"> <rotate x="1" y="0" z="0" angle="-90"/> <translate y="-10"/> </transform> <emitter type="area"> <spectrum name="radiance" value="1"/> </emitter> </shape> <!-- Polarizer --> <shape type="rectangle"> <bsdf type="polarizer"> </bsdf> <transform name="to_world"> <rotate x="1" y="0" z="0" angle="-90"/> <rotate y="1" angle="{}"/> </transform> </shape> </scene>""".format(angle) scene = load_string(scene_str) integrator = scene.integrator() sensor = scene.sensors()[0] sampler = sensor.sampler() # Sample ray from sensor ray, _ = sensor.sample_ray_differential(0.0, 0.5, [0.5, 0.5], [0.5, 0.5]) # Call integrator value, _, _ = integrator.sample(scene, sampler, ray) # Normalize Stokes vector value /= value[0, 0] # Align output stokes vector (based on ray.d) with optical table. (In this configuration, this is a no-op.) forward = -ray.d basis_cur = stokes_basis(forward) basis_tar = [-1, 0, 0] R = rotate_stokes_basis_m(forward, basis_cur, basis_tar) value = R @ value assert ek.allclose(value, expected[k], atol=1e-3)
def test02_sample_pol_local(variant_scalar_mono_polarized): from mitsuba.core import Frame3f, Transform4f, Spectrum, UnpolarizedSpectrum, Vector3f from mitsuba.core.xml import load_string from mitsuba.render import BSDFContext, TransportMode, SurfaceInteraction3f, fresnel_conductor from mitsuba.render.mueller import stokes_basis, rotate_mueller_basis def spectrum_from_stokes(v): res = Spectrum(0.0) for i in range(4): res[i, 0] = v[i] return res # Create a Silver conductor BSDF and reflect different polarization states # at a 45˚ angle. # All tests take place directly in local BSDF coordinates. Here we only # want to make sure that the output of this looks like what you would # expect from a Mueller matrix describing specular reflection on a mirror. eta = 0.136125 + 4.010625j # IoR values of Ag at 635.816284nm bsdf = load_string("""<bsdf version='2.0.0' type='conductor'> <spectrum name="eta" value="{}"/> <spectrum name="k" value="{}"/> </bsdf>""".format(eta.real, eta.imag)) theta_i = 45 * ek.pi / 180 wi = Vector3f([-ek.sin(theta_i), 0, ek.cos(theta_i)]) ctx = BSDFContext() ctx.mode = TransportMode.Importance si = SurfaceInteraction3f() si.p = [0, 0, 0] si.wi = wi n = [0, 0, 1] si.sh_frame = Frame3f(n) bs, M_local = bsdf.sample(ctx, si, 0.0, [0.0, 0.0]) wo = bs.wo # Rotate into standard coordinates for specular reflection bi_s = ek.normalize(ek.cross(n, -wi)) bi_p = ek.normalize(ek.cross(-wi, bi_s)) bo_s = ek.normalize(ek.cross(n, wo)) bo_p = ek.normalize(ek.cross(wo, bi_s)) M_local = rotate_mueller_basis(M_local, -wi, stokes_basis(-wi), bi_p, wo, stokes_basis(wo), bo_p) # Apply to unpolarized light and verify that it is equivalent to normal Fresnel a0 = M_local @ spectrum_from_stokes([1, 0, 0, 0]) F = fresnel_conductor(ek.cos(theta_i), ek.scalar.Complex2f(eta.real, eta.imag)) a0 = a0[0, 0] assert ek.allclose(a0[0], F, atol=1e-3) # Apply to horizontally polarized light (linear at 0˚) # Test that it is.. # 1) still fully polarized (though with reduced intensity) # 2) still horizontally polarized a1 = M_local @ spectrum_from_stokes([1, +1, 0, 0]) assert ek.allclose(a1[0, 0], ek.abs(a1[1, 0]), atol=1e-3) # 1) a1 /= a1[0, 0] assert ek.allclose(a1, spectrum_from_stokes([1, 1, 0, 0]), atol=1e-3) # 2) # Apply to vertically polarized light (linear at 90˚) # Test that it is.. # 1) still fully polarized (though with reduced intensity) # 2) still vertically polarized a2 = M_local @ spectrum_from_stokes([1, -1, 0, 0]) assert ek.allclose(a2[0, 0], ek.abs(a2[1, 0]), atol=1e-3) # 1) a2 /= a2[0, 0] assert ek.allclose(a2, spectrum_from_stokes([1, -1, 0, 0]), atol=1e-3) # 2) # Apply to (positive) diagonally polarized light (linear at +45˚) # Test that.. # 1) The polarization is flipped to -45˚ # 2) There is now also some (left) circular polarization a3 = M_local @ spectrum_from_stokes([1, 0, +1, 0]) assert ek.all(a3[2, 0] < UnpolarizedSpectrum(0.0)) assert ek.all(a3[3, 0] < UnpolarizedSpectrum(0.0)) # Apply to (negative) diagonally polarized light (linear at -45˚) # Test that.. # 1) The polarization is flipped to +45˚ # 2) There is now also some (right) circular polarization a4 = M_local @ spectrum_from_stokes([1, 0, -1, 0]) assert ek.all(a4[2, 0] > UnpolarizedSpectrum(0.0)) assert ek.all(a4[3, 0] > UnpolarizedSpectrum(0.0)) # Apply to right circularly polarized light # Test that the polarization is flipped to left circular a5 = M_local @ spectrum_from_stokes([1, 0, 0, +1]) assert ek.all(a5[3, 0] < UnpolarizedSpectrum(0.0)) # Apply to left circularly polarized light # Test that the polarization is flipped to right circular a6 = M_local @ spectrum_from_stokes([1, 0, 0, -1]) assert ek.all(a6[3, 0] > UnpolarizedSpectrum(0.0))
def test02_sample_pol_world(variant_scalar_mono_polarized): from mitsuba.core import Frame3f, Spectrum, UnpolarizedSpectrum from mitsuba.core.xml import load_string from mitsuba.render import BSDFContext, TransportMode, SurfaceInteraction3f, fresnel_conductor from mitsuba.render.mueller import stokes_basis, rotate_mueller_basis def spectrum_from_stokes(v): res = Spectrum(0.0) for i in range(4): res[i, 0] = v[i] return res # Create a Silver conductor BSDF and reflect different polarization states # at a 45˚ angle. # This test takes place in world coordinates and thus involves additional # coordinate system rotations. # # The setup is as follows: # - The mirror is positioned at [0, 0, 0], angled s.t. the surface normal # is along [1, 1, 0]. # - Incident ray w1 = [-1, 0, 0] strikes the mirror at a 45˚ angle and # reflects into direction w2 = [0, 1, 0] # - The corresponding Stokes bases are b1 = [0, 1, 0] and b2 = [1, 0, 0]. # Setup eta = 0.136125 + 4.010625j # IoR values of Ag at 635.816284nm bsdf = load_string("""<bsdf version='2.0.0' type='conductor'> <spectrum name="eta" value="{}"/> <spectrum name="k" value="{}"/> </bsdf>""".format(eta.real, eta.imag)) ctx = BSDFContext() ctx.mode = TransportMode.Importance si = SurfaceInteraction3f() si.p = [0, 0, 0] si.n = ek.normalize([1.0, 1.0, 0.0]) si.sh_frame = Frame3f(si.n) # Incident / outgoing directions and their Stokes bases w1 = ek.scalar.Vector3f([-1, 0, 0]) b1 = [0, 1, 0] w2 = ek.scalar.Vector3f([0, 1, 0]) b2 = [1, 0, 0] # Perform actual reflection si.wi = si.to_local(-w1) bs, M_tmp = bsdf.sample(ctx, si, 0.0, [0.0, 0.0]) M_world = si.to_world_mueller(M_tmp, -si.wi, bs.wo) # Test that outgoing direction is as predicted assert ek.allclose(si.to_world(bs.wo), w2, atol=1e-5) # Align reference frames s.t. polarization is expressed w.r.t. b1 & b2 M_world = rotate_mueller_basis(M_world, w1, stokes_basis(w1), b1, w2, stokes_basis(w2), b2) # Apply to unpolarized light and verify that it is equivalent to normal Fresnel a0 = M_world @ spectrum_from_stokes([1, 0, 0, 0]) F = fresnel_conductor(si.wi[2], ek.scalar.Complex2f(eta.real, eta.imag)) a0 = a0[0, 0] assert ek.allclose(a0[0], F, atol=1e-3) # Apply to horizontally polarized light (linear at 0˚) # Test that it is.. # 1) still fully polarized (though with reduced intensity) # 2) still horizontally polarized a1 = M_world @ spectrum_from_stokes([1, +1, 0, 0]) assert ek.allclose(a1[0, 0], ek.abs(a1[1, 0]), atol=1e-3) # 1) a1 /= a1[0, 0] assert ek.allclose(a1, spectrum_from_stokes([1, 1, 0, 0]), atol=1e-3) # 2) # Apply to vertically polarized light (linear at 90˚) # Test that it is.. # 1) still fully polarized (though with reduced intensity) # 2) still vertically polarized a2 = M_world @ spectrum_from_stokes([1, -1, 0, 0]) assert ek.allclose(a2[0, 0], ek.abs(a2[1, 0]), atol=1e-3) # 1) a2 /= a2[0, 0] assert ek.allclose(a2, spectrum_from_stokes([1, -1, 0, 0]), atol=1e-3) # 2) # Apply to (positive) diagonally polarized light (linear at +45˚) # Test that.. # 1) The polarization is flipped to -45˚ # 2) There is now also some (left) circular polarization a3 = M_world @ spectrum_from_stokes([1, 0, +1, 0]) assert ek.all(a3[2, 0] < UnpolarizedSpectrum(0.0)) assert ek.all(a3[3, 0] < UnpolarizedSpectrum(0.0)) # Apply to (negative) diagonally polarized light (linear at -45˚) # Test that.. # 1) The polarization is flipped to +45˚ # 2) There is now also some (right) circular polarization a4 = M_world @ spectrum_from_stokes([1, 0, -1, 0]) assert ek.all(a4[2, 0] > UnpolarizedSpectrum(0.0)) assert ek.all(a4[3, 0] > UnpolarizedSpectrum(0.0)) # Apply to right circularly polarized light # Test that the polarization is flipped to left circular a5 = M_world @ spectrum_from_stokes([1, 0, 0, +1]) assert ek.all(a5[3, 0] < UnpolarizedSpectrum(0.0)) # Apply to left circularly polarized light # Test that the polarization is flipped to right circular a6 = M_world @ spectrum_from_stokes([1, 0, 0, -1]) assert ek.all(a6[3, 0] > UnpolarizedSpectrum(0.0))