diff --git a/active_projects/quaternions.py b/active_projects/quaternions.py index c130bec9..191ee427 100644 --- a/active_projects/quaternions.py +++ b/active_projects/quaternions.py @@ -97,6 +97,12 @@ def stereo_project(mobject, axis=0, r=1, outer_r=10, **kwargs): return mobject +def updating_mobject_from_func(func): + mob = func() + mob.add_updater(lambda m: mob.become(func())) + return mob + + class Linus(VGroup): CONFIG = { "body_config": { @@ -288,6 +294,7 @@ class StereoProjectedSphereFromHypersphere(StereoProjectedSphere): "axis": 0, }, "radius": 2, + "multiply_from_right": False, } def __init__(self, quaternion=None, null_axis=0, **kwargs): @@ -301,7 +308,10 @@ class StereoProjectedSphereFromHypersphere(StereoProjectedSphere): def q_mult_projection_func(self, u, v): point = list(Sphere.func(self, u, v)) point.insert(self.null_axis, 0) - post_q_mult = q_mult(self.quaternion, point) + if self.multiply_from_right: + post_q_mult = q_mult(point, self.quaternion) + else: + post_q_mult = q_mult(self.quaternion, point) projected = list(self.radius * stereo_project_point( post_q_mult, **self.stereo_project_config )) @@ -316,12 +326,13 @@ class StereoProjectedCircleFromHypersphere(CheckeredCircle): CONFIG = { "n_pieces": 48, "radius": 2, - "max_length": 2 * FRAME_WIDTH, - "max_r": 20, + "max_length": 3 * FRAME_WIDTH, + "max_r": 50, "basis_vectors": [ [1, 0, 0, 0], [0, 1, 0, 0], - ] + ], + "multiply_from_right": False, } def __init__(self, quaternion=None, **kwargs): @@ -344,7 +355,10 @@ class StereoProjectedCircleFromHypersphere(CheckeredCircle): def projection(self, point): q1 = self.quaternion q2 = np.dot(self.pre_positioning_matrix, point[:2]).flatten() - new_q = q_mult(q1, q2) + if self.multiply_from_right: + new_q = q_mult(q2, q1) + else: + new_q = q_mult(q1, q2) projected = stereo_project_point( new_q, axis=0, r=self.radius, ) @@ -1118,6 +1132,74 @@ class RotateCubeThreeTimes(SpecialThreeDScene): self.wait(7) +class QuantumSpin(Scene): + def construct(self): + title = Title("Two-state system") + + electron = Dot(color=BLUE) + electron.set_height(1) + electron.set_sheen(0.3, UL) + electron.set_fill(opacity=0.8) + kwargs = { + "use_rectangular_stem": False, + "path_arc": PI, + } + curved_arrows = VGroup( + Arrow(RIGHT, LEFT, **kwargs), + Arrow(LEFT, RIGHT, **kwargs), + ) + curved_arrows.set_color(LIGHT_GREY) + curved_arrows.set_stroke(width=2) + + y_arrow = Vector(UP) + y_arrow.set_color(RED) + + y_ca = curved_arrows.copy() + y_ca.rotate(70 * DEGREES, LEFT) + y_group = VGroup(y_ca[0], y_arrow, electron.copy(), y_ca[1]) + + x_arrow = y_arrow.copy().rotate(90 * DEGREES, IN, about_point=ORIGIN) + x_arrow.set_color(GREEN) + x_ca = curved_arrows.copy() + x_ca.rotate(70 * DEGREES, UP) + x_group = VGroup(x_ca[0], x_arrow, electron.copy(), x_ca[1]) + + z_ca = curved_arrows.copy() + z_group = VGroup(electron.copy(), z_ca, Dot(color=BLUE)) + + groups = VGroup(x_group, y_group, z_group) + groups.arrange_submobjects(RIGHT, buff=1.5) + groups.move_to(UP) + + y_ca.rotation = Rotating( + y_ca, axis=rotate_vector(OUT, 70 * DEGREES, LEFT) + ) + x_ca.rotation = Rotating( + x_ca, axis=rotate_vector(OUT, 70 * DEGREES, UP) + ) + z_ca.rotation = Rotating(z_ca, axis=OUT) + rotations = [ca.rotation for ca in [x_ca, y_ca, z_ca]] + + matrices = VGroup( + Matrix([["0", "1"], ["1", "0"]]), + Matrix([["0", "-i"], ["i", "0"]]), + Matrix([["1", "0"], ["0", "-1"]]), + ) + for matrix, group in zip(matrices, groups): + matrix.next_to(group, DOWN) + for matrix in matrices[1:]: + matrix.align_to(matrices[0], DOWN) + + self.add(title, groups) + self.play() + self.play( + LaggedStart(FadeInFromDown, matrices, run_time=3), + *rotations + ) + for x in range(2): + self.play(*rotations) + + class HereWeTackle4d(TeacherStudentsScene): def construct(self): titles = VGroup( @@ -2252,6 +2334,7 @@ class OneDegreeOfFreedomForRotation(Scene): right_dot = Dot(color=WHITE) right_dot.move_to(circle.get_right()) circle.add(moving_r_line, right_dot) + center_dot = Dot(color=WHITE) def get_angle(): return moving_r_line.get_angle() % TAU @@ -2275,7 +2358,7 @@ class OneDegreeOfFreedomForRotation(Scene): arc = get_arc() arc.add_updater(lambda m: m.become(get_arc())) - self.add(circle, r_line, angle_label, arc) + self.add(circle, center_dot, r_line, angle_label, arc) angles = IntroduceStereographicProjection.CONFIG.get( "example_angles" ) @@ -2572,8 +2655,13 @@ class IntroduceStereographicProjection(MovingCameraScene): def get_sample_circle_points(self): plane = self.plane n = self.n_sample_points + rotater = 1 + if hasattr(self, "circle_shadow"): + point = self.circle_shadow[0].points[0] + rotater = complex(*point[:2]) + rotater /= abs(rotater) numbers = [ - np.exp(complex(0, TAU * x / n)) + rotater * np.exp(complex(0, TAU * x / n)) for x in range(-(n // 2) + 1, n // 2) ] return [ @@ -2591,11 +2679,9 @@ class IntroduceStereographicProjection(MovingCameraScene): for point in circle_points ]) for line in lines: - line.scale( - 20 / line.get_length(), - about_point=neg_one_point - ) - line.set_stroke(YELLOW, 1) + length = line.get_length() + line.scale(20 / length, about_point=neg_one_point) + line.set_stroke(YELLOW, np.clip(length, 0, 1)) return lines def project(self, point): @@ -2835,7 +2921,7 @@ class ShowRotationUnderStereographicProjection(IntroduceStereographicProjection) neg_one_dot = Dot(neg_one_point) neg_one_dot.set_fill(YELLOW) - lines = self.get_lines() + lines = updating_mobject_from_func(self.get_lines) def generate_dot_updater(circle_piece): return lambda d: d.move_to(circle_piece.points[0]) @@ -3146,13 +3232,23 @@ class IntroduceThreeDNumbers(SpecialThreeDScene): plane = self.plane - x_line = Line(LEFT, RIGHT).set_width(FRAME_WIDTH) + colored_ = Line(LEFT, RIGHT).set_width(FRAME_WIDTH) y_line = Line(DOWN, UP).set_height(FRAME_WIDTH) z_line = Line(IN, OUT).set_depth(FRAME_WIDTH) - x_line.set_stroke(GREEN, 5) + colored_.set_stroke(GREEN, 5) y_line.set_stroke(RED, 5) z_line.set_stroke(YELLOW, 5) - coord_lines = VGroup(x_line, y_line, z_line) + colored_coord_lines = VGroup(colored_, y_line, z_line) + + coord_lines = VGroup( + plane.axes[0], plane.axes[1], z_axis.main_line, + ) + for i1, i2 in [(0, 2), (1, 0), (2, 1)]: + coord_lines[i1].target = coord_lines[i2].copy() + + new_title = TextMobject("Imaginary plane") + new_title.replace(self.title) + new_title.move_to(self.title) self.add(z_axis, plane, z_axis_top) self.move_camera( @@ -3184,13 +3280,17 @@ class IntroduceThreeDNumbers(SpecialThreeDScene): label.target.next_to( 2 * y * UP, RIGHT, 2 * SMALL_BUFF ) + self.remove(z_axis_top) self.play( LaggedStart(MoveToTarget, group, lag_ratio=0.8), + LaggedStart(MoveToTarget, coord_lines, lag_ratio=0.8), FadeOut(self.title), + FadeIn(new_title), run_time=3 ) + self.add(z_axis_top) self.wait(3) - for line, wait in zip(coord_lines, [False, True, True]): + for line, wait in zip(colored_coord_lines, [False, True, True]): self.play( ShowCreationThenDestruction(line), run_time=2 @@ -3264,7 +3364,7 @@ class MentionImpossibilityOf3dNumbers(TeacherStudentsScene): why.next_to(self.students[1], UP) self.teacher_says( - "Such 3d number \\\\ have no good \\\\ multiplication rule", + "Such 3d numbers \\\\ have no good \\\\ multiplication rule", bubble_kwargs={"width": 4, "height": 3}, ) self.change_all_student_modes("confused") @@ -3360,6 +3460,7 @@ class TwoDStereographicProjection(IntroduceFelix): 0, TAU, TAU / 16, ), "n_sample_rotation_cycles": 2, + "lift_labels": True, } def construct(self): @@ -3376,7 +3477,7 @@ class TwoDStereographicProjection(IntroduceFelix): sphere = self.sphere = self.get_sphere() c2p = axes.coords_to_point - labels = VGroup( + labels = self.labels = VGroup( TexMobject("i").next_to(c2p(1, 0, 0), DR, SMALL_BUFF), TexMobject("-i").next_to(c2p(-1, 0, 0), DL, SMALL_BUFF), TexMobject("j").next_to(c2p(0, 1, 0), UL, SMALL_BUFF), @@ -3388,10 +3489,11 @@ class TwoDStereographicProjection(IntroduceFelix): 90 * DEGREES, RIGHT, ).next_to(c2p(0, 0, -1), RIGHT + IN, SMALL_BUFF), ) - for sm in labels[:4].family_members_with_points(): - sm.add(VectorizedPoint( - 0.25 * DOWN + 0.25 * OUT - )) + if self.lift_labels: + for sm in labels[:4].family_members_with_points(): + sm.add(VectorizedPoint( + 0.25 * DOWN + 0.25 * OUT + )) labels.set_stroke(width=0, background=True) for submob in labels.get_family(): submob.shade_in_3d = True @@ -3811,9 +3913,10 @@ class FelixViewOfProjection(TwoDStereographicProjection): TexMobject("-i"), TexMobject("j"), TexMobject("-j"), + TexMobject("1"), ) - coords = [(1, 0), (-1, 0), (0, 1), (0, -1)] - vects = [DOWN, DOWN, RIGHT, RIGHT] + coords = [(1, 0), (-1, 0), (0, 1), (0, -1), (0, 0)] + vects = [DOWN, DOWN, RIGHT, RIGHT, 0.25 * DR] for label, coords, vect in zip(labels, coords, vects): point = axes.coords_to_point(*coords) label.next_to(point, vect, buff=MED_SMALL_BUFF) @@ -3890,6 +3993,10 @@ class FelixViewOfProjection(TwoDStereographicProjection): class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): + CONFIG = { + "flat_view": False, + } + def construct(self): self.add_parts(run_time=1) self.begin_ambient_camera_rotation(rate=0.03) @@ -3925,6 +4032,8 @@ class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): labels = VGroup(*map(TexMobject, ["0", "2i", "3i"])) labels.set_shade_in_3d(True) + if self.flat_view: + labels.fade(1) for label, x in zip(labels, [0, 2, 3]): label.next_to( axes.coords_to_point(x, 0, 0), DR, SMALL_BUFF @@ -4086,6 +4195,8 @@ class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): circle = CheckeredCircle(colors=colors, n_pieces=48) circle.set_shade_in_3d(True) circle.match_width(sphere) + if self.flat_view: + circle[::2].fade(1) return circle @@ -4101,6 +4212,40 @@ class ShowRotationsJustWithReferenceCircles(TwoDStereographicProjection): return result +class ReferernceSpheresFelixView(ShowRotationsJustWithReferenceCircles): + CONFIG = { + "flat_view": True, + "lift_labels": False, + } + + def add_parts(self, **kwargs): + ShowRotationsJustWithReferenceCircles.add_parts(self, **kwargs) + one = TexMobject("1") + one.next_to(ORIGIN, DR, SMALL_BUFF) + self.add(one) + + def get_default_camera_position(self): + return {} + + def move_camera(self, **kwargs): + kwargs["phi"] = 0 + kwargs["theta"] = -90 * DEGREES + ShowRotationsJustWithReferenceCircles.move_camera(self, **kwargs) + + def begin_ambient_camera_rotation(self, rate): + self.ambient_camera_rotation = ContinualAnimation(VectorizedPoint()) + + def capture_mobjects_in_camera(self, mobjects, **kwargs): + mobs_on_xy = [ + sm + for sm in self.camera.extract_mobject_family_members( + mobjects, only_those_with_points=True + ) + if abs(sm.get_center()[2]) < 0.001 + ] + return Scene.capture_mobjects_in_camera(self, mobs_on_xy, **kwargs) + + class IntroduceQuaternions(Scene): def construct(self): self.compare_three_number_systems() @@ -4786,12 +4931,29 @@ class HypersphereStereographicProjection(SpecialThreeDScene): def show_quaternions_with_nonzero_real_part(self): # Positive real part - self.set_quat([1, 1, 2, 0]) + self.set_quat( + [1, 1, 2, 0], + added_anims=[ + ApplyMethod( + self.sphere.copy().scale, 0, + remover=True + ) + ] + ) self.wait(2) self.set_quat([4, 0, -1, -1]) self.wait(2) # Negative real part - self.set_quat([-1, 1, 2, 0]) + self.set_quat( + [-1, 1, 2, 0], + added_anims=[ + ApplyFunction( + lambda s: s.scale(10).fade(1), + self.sphere.copy(), + remover=True + ) + ] + ) self.wait(2) self.set_quat([-2, 0, -1, 1]) self.wait(2) @@ -5035,6 +5197,18 @@ class HypersphereStereographicProjection(SpecialThreeDScene): # return sphere +class MissingLabels(Scene): + def construct(self): + labels = VGroup( + TexMobject("i").move_to(UR), + TexMobject("1").move_to(0.3 * DOWN), + TexMobject("-i").move_to(DOWN + 1.2 * LEFT), + TexMobject("-j").move_to(1.7 * RIGHT + 0.8 * DOWN), + ) + labels.set_background_stroke(width=0) + self.add(labels) + + class RuleOfQuaternionMultiplicationOverlay(Scene): def construct(self): q_mob, times_mob, p_mob = q_times_p = TexMobject( @@ -5664,18 +5838,18 @@ class ShowJMultiplication(ShowMultiplicationBy135Example): circle = self.circle_ik.deepcopy() circle.clear_updaters() self.play(FadeInFromLarge(circle, remover=True)) - m_tracker.set_value([1, 0, 0, 0]) + m_tracker.set_value([-1, 0, 0, 0]) q_tracker.set_value([0, 1, 0, 0]) self.wait() self.play( - m_tracker.set_value, [0, 0, 1, 0], + m_tracker.set_value, [0, 0, -1, 0], q_tracker.set_value, [0, 0, 0, -1], *updates, run_time=run_time, ) self.wait(2) self.play( - m_tracker.set_value, [-1, 0, 1e-3, 0], + m_tracker.set_value, [1, 0, -1e-3, 0], q_tracker.set_value, [0, -1, 0, 0], *updates, run_time=run_time, @@ -5913,6 +6087,220 @@ class RotationsOfCube(SpecialThreeDScene): self.wait(5) +class OneFinalPoint(TeacherStudentsScene): + def construct(self): + self.teacher_says("One final point!") + self.change_student_modes("happy", "tease", "thinking") + self.wait(3) + + +class MultiplicationFromTheRight(Scene): + def construct(self): + i, dot, j = product = TexMobject("i", "\\cdot", "j") + product.set_height(1.5) + product.to_edge(UP, buff=LARGE_BUFF) + i.set_color(GREEN) + j.set_color(RED) + i_rect = SurroundingRectangle(i) + j_rect = SurroundingRectangle(j) + i_rect.match_height(j_rect, about_edge=UP, stretch=True) + VGroup(i_rect, j_rect).set_color(WHITE) + action_words = TextMobject("Think of as \\\\ an action") + point_words = TextMobject("Think of as \\\\ a point") + action_words.next_to(i_rect, LEFT) + point_words.next_to(j_rect, RIGHT) + + arrow = Arrow( + i.get_top(), + j.get_top(), + path_arc=-PI, + use_rectangular_stem=False, + ) + arrow.set_stroke(width=2) + + self.add(product) + self.play(ShowCreation(arrow)) + self.play( + FadeIn(i_rect), + Write(action_words) + ) + self.wait() + self.play( + FadeIn(j_rect), + Write(point_words) + ) + self.wait(2) + self.play(arrow.flip) + self.play(Swap(point_words, action_words)) + + +class MultiplicationByJFromRight(ShowJMultiplication): + def show_multiplication(self): + self.set_camera_orientation(theta=-80 * DEGREES) + + q_tracker = self.q_tracker + m_tracker = self.multiplier_tracker + + def normalize_tracker(t): + t.set_value(normalize(t.get_value())) + + updates = [ + UpdateFromFunc(tracker, normalize_tracker) + for tracker in (q_tracker, m_tracker) + ] + + run_time = self.run_time_per_rotation + + m_values = [[1, 0, 0, 0]] + q_values = [[0, 1, 0, 0]] + for values in m_values, q_values: + for x in range(4): + values.append( + q_mult(values[-1], [0, 0, 1, 0]) + ) + + for m_val, q_val in zip(m_values, q_values): + self.play( + m_tracker.set_value, m_val, + q_tracker.set_value, q_val, + *updates, + run_time=run_time, + ) + self.wait(2) + + def get_projected_sphere(self, *args, **kwargs): + kwargs["multiply_from_right"] = True + return ShowJMultiplication.get_projected_sphere( + self, *args, **kwargs + ) + + def get_projected_circle(self, *args, **kwargs): + kwargs["multiply_from_right"] = True + return ShowJMultiplication.get_projected_circle( + self, *args, **kwargs + ) + + +class HowQuaternionsRotate3dPoints(Scene): + def construct(self): + title = TextMobject( + "Coming up:\\\\", + "How quaternions act on 3d points" + ) + title.to_edge(UP) + + expression = TexMobject( + "q", "\\cdot", "p", "\\cdot", "q^{-1}" + ) + expression.scale(2) + expression.set_color_by_tex("q", PINK) + expression.set_color_by_tex("p", YELLOW) + + right_arrow = Arrow( + expression[0].get_top(), + expression[2].get_top(), + path_arc=-PI, + use_rectangular_stem=False, + color=WHITE, + ) + right_arrow.set_stroke(width=4) + left_arrow = right_arrow.copy() + left_arrow.flip(about_point=expression[2].get_top()) + left_arrow.shift(SMALL_BUFF * RIGHT) + + self.add(title) + self.play(Write(expression)) + self.play(ShowCreation(right_arrow)) + self.play(ShowCreation(left_arrow)) + self.wait() + + +class HoldUpQuanta(TeacherStudentsScene): + def construct(self): + logo = ImageMobject("Quanta logo") + logo.set_width(6) + logo.next_to(self.teacher, UR) + logo.to_edge(RIGHT, buff=2) + + words = TextMobject("Associated quaternion post") + words.to_edge(UP, buff=LARGE_BUFF) + arrow = Arrow(logo.get_top(), words.get_bottom()) + + self.play( + FadeInFromDown(logo), + self.teacher.change, "raise_right_hand" + ) + self.change_all_student_modes("hooray") + self.play( + GrowArrow(arrow), + GrowFromPoint(words, arrow.get_start()) + ) + self.wait(3) + + +class ShareWithFriends(PiCreatureScene): + def construct(self): + pi1, pi2 = self.pi_creatures + + self.pi_creature_says( + pi1, "Come learn about \\\\ quaternions!", + bubble_kwargs={ + "direction": LEFT, + "width": 4, + "height": 3, + }, + target_mode="hooray", + look_at_arg=pi2.eyes, + added_anims=[ + ApplyMethod( + pi2.change, "confused", + run_time=1.5, + rate_func=squish_rate_func(smooth, 0.3, 1) + ) + ] + ) + bubble = ThoughtBubble( + direction=RIGHT, + width=4, + height=4, + ) + bubble.pin_to(pi2) + bubble.write("What's that, some\\\\kind of particle?") + bubble.resize_to_content() + VGroup(bubble, bubble.content).shift_onto_screen(buff=SMALL_BUFF) + self.play( + pi2.look_at, pi1.eyes, + pi1.look_at, pi2.eyes, + ShowCreation(bubble), + Write(bubble.content), + ) + self.wait(1) + self.play( + RemovePiCreatureBubble(pi1, target_mode="raise_right_hand") + ) + self.wait() + + time_words = TextMobject("Oh man...\\\\30 minutes") + time_words.move_to(bubble.content) + self.play( + Animation(VectorizedPoint().next_to(pi1, UL, LARGE_BUFF)), + pi2.change, "sad", + FadeOutAndShift(bubble.content, DOWN), + FadeInFromDown(time_words, DOWN), + ) + self.wait(7) + + def create_pi_creatures(self): + pi1 = PiCreature(color=BLUE) + pi1.to_edge(DOWN) + pi1.flip() + pi1.shift(2.5 * RIGHT) + pi2 = PiCreature(color=RED) + pi2.to_edge(DOWN) + pi2.shift(2 * LEFT) + return [pi1, pi2] + + class QuaternionEndscreen(PatreonEndScreen): CONFIG = { "specific_patrons": [ @@ -6000,7 +6388,7 @@ class QuaternionEndscreen(PatreonEndScreen): "Dave B", "Delton Ding", "Thomas Tarler", - "1st", + "1stViewMaths", "Jacob Magnuson", "Clark Gaebel", "Mathias Jansson",