text: from scipy import integrate
  
  from manimlib.imports import *
  from active_projects.diffyq.part2.heat_equation import *
  
  
  class TemperatureGraphScene(SpecialThreeDScene):
      CONFIG = {
          "axes_config": {
              "x_min": 0,
              "x_max": TAU,
              "y_min": 0,
              "y_max": 10,
              "z_min": -3,
              "z_max": 3,
              "x_axis_config": {
                  "tick_frequency": TAU / 8,
                  "include_tip": False,
              },
              "num_axis_pieces": 1,
          },
          "default_graph_style": {
              "stroke_width": 2,
              "stroke_color": WHITE,
              "background_image_file": "VerticalTempGradient",
          },
          "default_surface_config": {
              "fill_opacity": 0.1,
              "checkerboard_colors": [LIGHT_GREY],
              "stroke_width": 0.5,
              "stroke_color": WHITE,
              "stroke_opacity": 0.5,
          },
          "temp_text": "Temperature",
      }
  
      def get_three_d_axes(self, include_labels=True, include_numbers=False, **kwargs):
          config = dict(self.axes_config)
          config.update(kwargs)
          axes = ThreeDAxes(**config)
          axes.set_stroke(width=2)
  
          if include_numbers:
              self.add_axes_numbers(axes)
  
          if include_labels:
              self.add_axes_labels(axes)
  
          # Adjust axis orientation
          axes.x_axis.rotate(
              90 * DEGREES, RIGHT,
              about_point=axes.c2p(0, 0, 0),
          )
          axes.y_axis.rotate(
              90 * DEGREES, UP,
              about_point=axes.c2p(0, 0, 0),
          )
  
          # Add xy-plane
          input_plane = self.get_surface(
              axes, lambda x, t: 0
          )
          input_plane.set_style(
              fill_opacity=0.5,
              fill_color=BLUE_B,
              stroke_width=0.5,
              stroke_color=WHITE,
          )
  
          axes.input_plane = input_plane
  
          return axes
  
      def add_axes_numbers(self, axes):
          x_axis = axes.x_axis
          y_axis = axes.y_axis
          tex_vals = [
              ("\\pi \\over 2", TAU / 4),
              ("\\pi", TAU / 2),
              ("3\\pi \\over 2", 3 * TAU / 4),
              ("2\\pi", TAU)
          ]
          x_labels = VGroup()
          for tex, val in tex_vals:
              label = TexMobject(tex)
              label.scale(0.5)
              label.next_to(x_axis.n2p(val), DOWN)
              x_labels.add(label)
          x_axis.add(x_labels)
          x_axis.numbers = x_labels
  
          y_axis.add_numbers()
          for number in y_axis.numbers:
              number.rotate(90 * DEGREES)
          return axes
  
      def add_axes_labels(self, axes):
          x_label = TexMobject("x")
          x_label.next_to(axes.x_axis.get_end(), RIGHT)
          axes.x_axis.label = x_label
  
          t_label = TextMobject("Time")
          t_label.rotate(90 * DEGREES, OUT)
          t_label.next_to(axes.y_axis.get_end(), UP)
          axes.y_axis.label = t_label
  
          temp_label = TextMobject(self.temp_text)
          temp_label.rotate(90 * DEGREES, RIGHT)
          temp_label.next_to(axes.z_axis.get_zenith(), RIGHT)
          axes.z_axis.label = temp_label
          for axis in axes:
              axis.add(axis.label)
          return axes
  
      def get_time_slice_graph(self, axes, func, t, **kwargs):
          config = dict()
          config.update(self.default_graph_style)
          config.update({
              "t_min": axes.x_min,
              "t_max": axes.x_max,
          })
          config.update(kwargs)
          return ParametricFunction(
              lambda x: axes.c2p(
                  x, t, func(x, t)
              ),
              **config,
          )
  
      def get_initial_state_graph(self, axes, func, **kwargs):
          return self.get_time_slice_graph(
              axes,
              lambda x, t: func(x),
              t=0,
              **kwargs
          )
  
      def get_surface(self, axes, func, **kwargs):
          config = {
              "u_min": axes.y_min,
              "u_max": axes.y_max,
              "v_min": axes.x_min,
              "v_max": axes.x_max,
              "resolution": (
                  (axes.y_max - axes.y_min) // axes.y_axis.tick_frequency,
                  (axes.x_max - axes.x_min) // axes.x_axis.tick_frequency,
              ),
          }
          config.update(self.default_surface_config)
          config.update(kwargs)
          return ParametricSurface(
              lambda t, x: axes.c2p(
                  x, t, func(x, t)
              ),
              **config
          )
  
      def orient_three_d_mobject(self, mobject,
                                 phi=85 * DEGREES,
                                 theta=-80 * DEGREES):
          mobject.rotate(-90 * DEGREES - theta, OUT)
          mobject.rotate(phi, LEFT)
          return mobject
  
      def get_rod_length(self):
          return self.axes_config["x_max"]
  
      def get_const_time_plane(self, axes):
          t_tracker = ValueTracker(0)
          plane = Polygon(
              *[
                  axes.c2p(x, 0, z)
                  for x, z in [
                      (axes.x_min, axes.z_min),
                      (axes.x_max, axes.z_min),
                      (axes.x_max, axes.z_max),
                      (axes.x_min, axes.z_max),
                  ]
              ],
              stroke_width=0,
              fill_color=WHITE,
              fill_opacity=0.2
          )
          plane.add_updater(lambda m: m.shift(
              axes.c2p(
                  axes.x_min,
                  t_tracker.get_value(),
                  axes.z_min,
              ) - plane.points[0]
          ))
          plane.t_tracker = t_tracker
          return plane
  
  
  class SimpleCosExpGraph(TemperatureGraphScene):
      def construct(self):
          axes = self.get_three_d_axes()
          cos_graph = self.get_cos_graph(axes)
          cos_exp_surface = self.get_cos_exp_surface(axes)
  
          self.set_camera_orientation(
              phi=80 * DEGREES,
              theta=-80 * DEGREES,
          )
          self.camera.frame_center.shift(3 * RIGHT)
          self.begin_ambient_camera_rotation(rate=0.01)
  
          self.add(axes)
          self.play(ShowCreation(cos_graph))
          self.play(UpdateFromAlphaFunc(
              cos_exp_surface,
              lambda m, a: m.become(
                  self.get_cos_exp_surface(axes, v_max=a * 10)
              ),
              run_time=3
          ))
  
          self.add(cos_graph.copy())
  
          t_tracker = ValueTracker(0)
          get_t = t_tracker.get_value
          cos_graph.add_updater(
              lambda m: m.become(self.get_time_slice_graph(
                  axes,
                  lambda x: self.cos_exp(x, get_t()),
                  t=get_t()
              ))
          )
  
          plane = Rectangle(
              stroke_width=0,
              fill_color=WHITE,
              fill_opacity=0.1,
          )
          plane.rotate(90 * DEGREES, RIGHT)
          plane.match_width(axes.x_axis)
          plane.match_depth(axes.z_axis, stretch=True)
          plane.move_to(axes.c2p(0, 0, 0), LEFT)
  
          self.add(plane, cos_graph)
          self.play(
              ApplyMethod(
                  t_tracker.set_value, 10,
                  run_time=10,
                  rate_func=linear,
              ),
              ApplyMethod(
                  plane.shift, 10 * UP,
                  run_time=10,
                  rate_func=linear,
              ),
              VFadeIn(plane),
          )
          self.wait(10)
  
      #
      def cos_exp(self, x, t, A=2, omega=1.5, k=0.1):
          return A * np.cos(omega * x) * np.exp(-k * (omega**2) * t)
  
      def get_cos_graph(self, axes, **config):
          return self.get_initial_state_graph(
              axes,
              lambda x: self.cos_exp(x, 0),
              **config
          )
  
      def get_cos_exp_surface(self, axes, **config):
          return self.get_surface(
              axes,
              lambda x, t: self.cos_exp(x, t),
              **config
          )
  
  
  class AddMultipleSolutions(SimpleCosExpGraph):
      CONFIG = {
          "axes_config": {
              "x_axis_config": {
                  "unit_size": 0.7,
              },
          }
      }
  
      def construct(self):
          axes1, axes2, axes3 = all_axes = VGroup(*[
              self.get_three_d_axes(
                  include_labels=False,
              )
              for x in range(3)
          ])
          all_axes.scale(0.5)
          self.orient_three_d_mobject(all_axes)
  
          As = [1.5, 1.5]
          omegas = [1.5, 2.5]
          ks = [0.1, 0.1]
          quads = [
              (axes1, [As[0]], [omegas[0]], [ks[0]]),
              (axes2, [As[1]], [omegas[1]], [ks[1]]),
              (axes3, As, omegas, ks),
          ]
  
          for axes, As, omegas, ks in quads:
              graph = self.get_initial_state_graph(
                  axes,
                  lambda x: np.sum([
                      self.cos_exp(x, 0, A, omega, k)
                      for A, omega, k in zip(As, omegas, ks)
                  ])
              )
              surface = self.get_surface(
                  axes,
                  lambda x, t: np.sum([
                      self.cos_exp(x, t, A, omega, k)
                      for A, omega, k in zip(As, omegas, ks)
                  ])
              )
              surface.sort(lambda p: -p[2])
  
              axes.add(surface, graph)
              axes.graph = graph
              axes.surface = surface
  
          self.set_camera_orientation(distance=100)
          plus = TexMobject("+").scale(2)
          equals = TexMobject("=").scale(2)
          group = VGroup(
              axes1, plus, axes2, equals, axes3,
          )
          group.arrange(RIGHT, buff=SMALL_BUFF)
  
          for axes in all_axes:
              checkmark = TexMobject("\\checkmark")
              checkmark.set_color(GREEN)
              checkmark.scale(2)
              checkmark.next_to(axes, UP)
              checkmark.shift(0.7 * DOWN)
              axes.checkmark = checkmark
  
          self.add(axes1, axes2)
          self.play(
              LaggedStart(
                  Write(axes1.surface),
                  Write(axes2.surface),
              ),
              LaggedStart(
                  FadeInFrom(axes1.checkmark, DOWN),
                  FadeInFrom(axes2.checkmark, DOWN),
              ),
              lag_ratio=0.2,
              run_time=1,
          )
          self.wait()
          self.play(Write(plus))
          self.play(
              Transform(
                  axes1.copy().set_fill(opacity=0),
                  axes3
              ),
              Transform(
                  axes2.copy().set_fill(opacity=0),
                  axes3
              ),
              FadeInFrom(equals, LEFT)
          )
          self.play(
              FadeInFrom(axes3.checkmark, DOWN),
          )
          self.wait()
  
  
  class BreakDownAFunction(SimpleCosExpGraph):
      CONFIG = {
          "axes_config": {
              "z_axis_config": {
                  "unit_size": 0.75,
                  "include_tip": False,
              },
              "z_min": -2,
              "y_max": 20,
          },
          "low_axes_config": {
              "z_min": -3,
              "z_axis_config": {"unit_size": 1}
          },
          "n_low_axes": 4,
          "k": 0.2,
      }
  
      def construct(self):
          self.set_camera_orientation(distance=100)
          self.set_axes()
          self.setup_graphs()
          self.show_break_down()
          self.show_solutions_for_waves()
  
      def set_axes(self):
          top_axes = self.get_three_d_axes()
          top_axes.z_axis.label.next_to(
              top_axes.z_axis.get_end(), OUT, SMALL_BUFF
          )
          top_axes.y_axis.set_opacity(0)
          self.orient_three_d_mobject(top_axes)
          top_axes.y_axis.label.rotate(-10 * DEGREES, UP)
          top_axes.scale(0.75)
          top_axes.center()
          top_axes.to_edge(UP)
  
          low_axes = self.get_three_d_axes(**self.low_axes_config)
          low_axes.y_axis.set_opacity(0)
          for axis in low_axes:
              axis.label.fade(1)
          # low_axes.add(low_axes.input_plane)
          # low_axes.input_plane.set_opacity(0)
  
          self.orient_three_d_mobject(low_axes)
          low_axes_group = VGroup(*[
              low_axes.deepcopy()
              for x in range(self.n_low_axes)
          ])
          low_axes_group.arrange(
              RIGHT, buff=low_axes.get_width() / 3
          )
          low_axes_group.set_width(FRAME_WIDTH - 2.5)
          low_axes_group.next_to(top_axes, DOWN, LARGE_BUFF)
          low_axes_group.to_edge(LEFT)
  
          self.top_axes = top_axes
          self.low_axes_group = low_axes_group
  
      def setup_graphs(self):
          top_axes = self.top_axes
          low_axes_group = self.low_axes_group
  
          top_graph = self.get_initial_state_graph(
              top_axes,
              self.initial_func,
              discontinuities=self.get_initial_func_discontinuities(),
              color=YELLOW,
          )
          top_graph.set_stroke(width=4)
  
          fourier_terms = self.get_fourier_cosine_terms(
              self.initial_func
          )
  
          low_graphs = VGroup(*[
              self.get_initial_state_graph(
                  axes,
                  lambda x: A * np.cos(n * x / 2)
              )
              for n, axes, A in zip(
                  it.count(),
                  low_axes_group,
                  fourier_terms
              )
          ])
          k = self.k
          low_surfaces = VGroup(*[
              self.get_surface(
                  axes,
                  lambda x, t: np.prod([
                      A,
                      np.cos(n * x / 2),
                      np.exp(-k * (n / 2)**2 * t)
                  ])
              )
              for n, axes, A in zip(
                  it.count(),
                  low_axes_group,
                  fourier_terms
              )
          ])
          top_surface = self.get_surface(
              top_axes,
              lambda x, t: np.sum([
                  np.prod([
                      A,
                      np.cos(n * x / 2),
                      np.exp(-k * (n / 2)**2 * t)
                  ])
                  for n, A in zip(
                      it.count(),
                      fourier_terms
                  )
              ])
          )
  
          self.top_graph = top_graph
          self.low_graphs = low_graphs
          self.low_surfaces = low_surfaces
          self.top_surface = top_surface
  
      def show_break_down(self):
          top_axes = self.top_axes
          low_axes_group = self.low_axes_group
          top_graph = self.top_graph
          low_graphs = self.low_graphs
  
          plusses = VGroup(*[
              TexMobject("+").next_to(
                  axes.x_axis.get_end(),
                  RIGHT, MED_SMALL_BUFF
              )
              for axes in low_axes_group
          ])
          dots = TexMobject("\\cdots")
          dots.next_to(plusses, RIGHT, MED_SMALL_BUFF)
  
          arrow = Arrow(
              dots.get_right(),
              top_graph.get_end() + 1.4 * DOWN + 1.7 * RIGHT,
              path_arc=90 * DEGREES,
          )
  
          top_words = TextMobject("Arbitrary\\\\function")
          top_words.next_to(top_axes, LEFT, MED_LARGE_BUFF)
          top_words.set_color(YELLOW)
          top_arrow = Arrow(
              top_words.get_right(),
              top_graph.point_from_proportion(0.3)
          )
  
          low_words = TextMobject("Sine curves")
          low_words.set_color(BLUE)
          low_words.next_to(low_axes_group, DOWN, MED_LARGE_BUFF)
  
          self.add(top_axes)
          self.play(ShowCreation(top_graph))
          self.play(
              FadeInFrom(top_words, RIGHT),
              ShowCreation(top_arrow)
          )
          self.wait()
          self.play(
              LaggedStartMap(FadeIn, low_axes_group),
              FadeInFrom(low_words, UP),
              LaggedStartMap(FadeInFromDown, [*plusses, dots]),
              *[
                  TransformFromCopy(top_graph, low_graph)
                  for low_graph in low_graphs
              ],
          )
          self.play(ShowCreation(arrow))
          self.wait()
  
      def show_solutions_for_waves(self):
          low_axes_group = self.low_axes_group
          top_axes = self.top_axes
          low_graphs = self.low_graphs
          low_surfaces = self.low_surfaces
          top_surface = self.top_surface
          top_graph = self.top_graph
  
          for surface in [top_surface, *low_surfaces]:
              surface.sort(lambda p: -p[2])
  
          anims1 = []
          anims2 = [
              ApplyMethod(
                  top_axes.y_axis.set_opacity, 1,
              ),
          ]
          for axes, surface, graph in zip(low_axes_group, low_surfaces, low_graphs):
              axes.y_axis.set_opacity(1)
              axes.y_axis.label.fade(1)
              anims1 += [
                  ShowCreation(axes.y_axis),
                  Write(surface, run_time=2),
              ]
              anims2.append(AnimationGroup(
                  TransformFromCopy(graph, top_graph.copy()),
                  Transform(
                      surface.copy().set_fill(opacity=0),
                      top_surface,
                  )
              ))
  
          self.play(*anims1)
          self.wait()
          self.play(LaggedStart(*anims2, run_time=2))
          self.wait()
  
          checkmark = TexMobject("\\checkmark")
          checkmark.set_color(GREEN)
          low_checkmarks = VGroup(*[
              checkmark.copy().next_to(
                  surface.get_top(), UP, SMALL_BUFF
              )
              for surface in low_surfaces
          ])
          top_checkmark = checkmark.copy()
          top_checkmark.scale(1.5)
          top_checkmark.move_to(top_axes.get_corner(UR))
  
          self.play(LaggedStartMap(FadeInFromDown, low_checkmarks))
          self.wait()
          self.play(*[
              TransformFromCopy(low_checkmark, top_checkmark.copy())
              for low_checkmark in low_checkmarks
          ])
          self.wait()
  
      #
      def initial_func(self, x):
          # return 3 * np.exp(-(x - PI)**2)
  
          x1 = TAU / 4 - 1
          x2 = TAU / 4 + 1
          x3 = 3 * TAU / 4 - 1.6
          x4 = 3 * TAU / 4 + 0.3
  
          T0 = -2
          T1 = 2
          T2 = 1
  
          if x < x1:
              return T0
          elif x < x2:
              alpha = inverse_interpolate(x1, x2, x)
              return bezier([T0, T0, T1, T1])(alpha)
          elif x < x3:
              return T1
          elif x < x4:
              alpha = inverse_interpolate(x3, x4, x)
              return bezier([T1, T1, T2, T2])(alpha)
          else:
              return T2
  
      def get_initial_func_discontinuities(self):
          # return [TAU / 4, 3 * TAU / 4]
          return []
  
      def get_fourier_cosine_terms(self, func, n_terms=40):
          result = [
              integrate.quad(
                  lambda x: (1 / PI) * func(x) * np.cos(n * x / 2),
                  0, TAU
              )[0]
              for n in range(n_terms)
          ]
          result[0] = result[0] / 2
          return result
  
  
  class OceanOfPossibilities(TemperatureGraphScene):
      CONFIG = {
          "axes_config": {
              "z_min": 0,
              "z_max": 4,
          },
          "k": 0.2,
          "default_surface_config": {
              # "resolution": (32, 20),
              # "resolution": (8, 5),
          }
      }
  
      def construct(self):
          self.setup_camera()
          self.setup_axes()
          self.setup_surface()
          self.show_solution()
          self.reference_boundary_conditions()
          self.reference_initial_condition()
          self.ambiently_change_solution()
  
      def setup_camera(self):
          self.set_camera_orientation(
              phi=80 * DEGREES,
              theta=-80 * DEGREES,
          )
          self.begin_ambient_camera_rotation(rate=0.01)
  
      def setup_axes(self):
          axes = self.get_three_d_axes(include_numbers=True)
          axes.add(axes.input_plane)
          axes.scale(0.8)
          axes.center()
          axes.shift(OUT + RIGHT)
  
          self.add(axes)
          self.axes = axes
  
      def setup_surface(self):
          axes = self.axes
          k = self.k
  
          # Parameters for surface function
          initial_As = [2] + [
              0.8 * random.choice([-1, 1]) / n
              for n in range(1, 20)
          ]
          A_trackers = Group(*[
              ValueTracker(A)
              for A in initial_As
          ])
  
          def get_As():
              return [At.get_value() for At in A_trackers]
  
          omegas = [n / 2 for n in range(0, 10)]
  
          def func(x, t):
              return np.sum([
                  np.prod([
                      A * np.cos(omega * x),
                      np.exp(-k * omega**2 * t)
                  ])
                  for A, omega in zip(get_As(), omegas)
              ])
  
          # Surface and graph
          surface = always_redraw(
              lambda: self.get_surface(axes, func)
          )
          t_tracker = ValueTracker(0)
          graph = always_redraw(
              lambda: self.get_time_slice_graph(
                  axes, func, t_tracker.get_value(),
              )
          )
  
          surface.suspend_updating()
          graph.suspend_updating()
  
          self.surface_func = func
          self.surface = surface
          self.graph = graph
          self.t_tracker = t_tracker
          self.A_trackers = A_trackers
          self.omegas = omegas
  
      def show_solution(self):
          axes = self.axes
          surface = self.surface
          graph = self.graph
          t_tracker = self.t_tracker
          get_t = t_tracker.get_value
  
          opacity_tracker = ValueTracker(0)
          plane = always_redraw(lambda: Polygon(
              *[
                  axes.c2p(x, get_t(), T)
                  for x, T in [
                      (0, 0), (TAU, 0), (TAU, 4), (0, 4)
                  ]
              ],
              stroke_width=0,
              fill_color=WHITE,
              fill_opacity=opacity_tracker.get_value(),
          ))
  
          self.add(surface, plane, graph)
          graph.resume_updating()
          self.play(
              opacity_tracker.set_value, 0.2,
              ApplyMethod(
                  t_tracker.set_value, 1,
                  rate_func=linear
              ),
              run_time=1
          )
          self.play(
              ApplyMethod(
                  t_tracker.set_value, 10,
                  rate_func=linear,
                  run_time=9
              )
          )
          self.wait()
  
          self.plane = plane
  
      def reference_boundary_conditions(self):
          axes = self.axes
          t_numbers = axes.y_axis.numbers
  
          lines = VGroup(*[
              Line(
                  axes.c2p(x, 0, 0),
                  axes.c2p(x, axes.y_max, 0),
                  stroke_width=3,
                  stroke_color=MAROON_B,
              )
              for x in [0, axes.x_max]
          ])
          surface_boundary_lines = always_redraw(lambda: VGroup(*[
              ParametricFunction(
                  lambda t: axes.c2p(
                      x, t,
                      self.surface_func(x, t)
                  ),
                  t_max=axes.y_max
              ).match_style(self.graph)
              for x in [0, axes.x_max]
          ]))
          # surface_boundary_lines.suspend_updating()
          words = VGroup()
          for line in lines:
              word = TextMobject("Boundary")
              word.set_stroke(BLACK, 3, background=True)
              word.scale(1.5)
              word.match_color(line)
              word.rotate(90 * DEGREES, RIGHT)
              word.rotate(90 * DEGREES, OUT)
              word.next_to(line, OUT, SMALL_BUFF)
              words.add(word)
  
          self.stop_ambient_camera_rotation()
          self.move_camera(
              theta=-45 * DEGREES,
              added_anims=[
                  LaggedStartMap(ShowCreation, lines),
                  LaggedStartMap(
                      FadeInFrom, words,
                      lambda m: (m, IN)
                  ),
                  FadeOut(t_numbers),
              ]
          )
          self.play(
              LaggedStart(*[
                  TransformFromCopy(l1, l2)
                  for l1, l2 in zip(lines, surface_boundary_lines)
              ])
          )
          self.add(surface_boundary_lines)
          self.wait()
          self.move_camera(
              theta=-70 * DEGREES,
          )
  
          self.surface_boundary_lines = surface_boundary_lines
  
      def reference_initial_condition(self):
          plane = self.plane
          t_tracker = self.t_tracker
  
          self.play(
              t_tracker.set_value, 0,
              run_time=2
          )
          plane.clear_updaters()
          self.play(FadeOut(plane))
  
      def ambiently_change_solution(self):
          A_trackers = self.A_trackers
  
          def generate_A_updater(A, rate):
              def update(m, dt):
                  m.total_time += dt
                  m.set_value(
                      2 * A * np.sin(rate * m.total_time + PI / 6)
                  )
              return update
  
          rates = [0, 0.2] + [
              0.5 + 0.5 * np.random.random()
              for x in range(len(A_trackers) - 2)
          ]
  
          for tracker, rate in zip(A_trackers, rates):
              tracker.total_time = 0
              tracker.add_updater(generate_A_updater(
                  tracker.get_value(),
                  rate
              ))
  
          self.add(*A_trackers)
          self.surface_boundary_lines.resume_updating()
          self.surface.resume_updating()
          self.graph.resume_updating()
          self.begin_ambient_camera_rotation(rate=0.01)
          self.wait(30)
  
  
  class AnalyzeSineCurve(TemperatureGraphScene):
      CONFIG = {
          "origin_point": 3 * LEFT,
          "axes_config": {
              "z_min": -1.5,
              "z_max": 1.5,
              "z_axis_config": {
                  "unit_size": 2,
                  "tick_frequency": 0.5,
              }
          },
          "tex_to_color_map": {
              "{x}": GREEN,
              "T": YELLOW,
              "=": WHITE,
              "0": WHITE,
              "\\Delta t": WHITE,
              "\\sin": WHITE,
              "{t}": PINK,
          }
      }
  
      def construct(self):
          self.setup_axes()
          self.ask_about_sine_curve()
          self.show_sine_wave_on_axes()
          self.reference_curvature()
          self.show_derivatives()
          self.show_curvature_matching_height()
          self.show_time_step_scalings()
          self.smooth_evolution()
  
      def setup_axes(self):
          axes = self.get_three_d_axes()
          axes.rotate(90 * DEGREES, LEFT)
          axes.shift(self.origin_point - axes.c2p(0, 0, 0))
          y_axis = axes.y_axis
          y_axis.fade(1)
          z_axis = axes.z_axis
          z_axis.label.next_to(z_axis.get_end(), UP, SMALL_BUFF)
  
          self.add_axes_numbers(axes)
          y_axis.remove(y_axis.numbers)
          axes.z_axis.add_numbers(
              *range(-1, 2),
              direction=LEFT,
          )
  
          self.axes = axes
  
      def ask_about_sine_curve(self):
          curve = FunctionGraph(
              lambda t: np.sin(t),
              x_min=0,
              x_max=TAU,
          )
          curve.move_to(DR)
          curve.set_width(5)
          curve.set_color(YELLOW)
          question = TextMobject("What's so special?")
          question.scale(1.5)
          question.to_edge(UP)
          question.shift(2 * LEFT)
          arrow = Arrow(
              question.get_bottom(),
              curve.point_from_proportion(0.25)
          )
  
          self.play(
              ShowCreation(curve),
              Write(question, run_time=1),
              GrowArrow(arrow),
          )
          self.wait()
  
          self.quick_sine_curve = curve
          self.question_group = VGroup(question, arrow)
  
      def show_sine_wave_on_axes(self):
          axes = self.axes
          graph = self.get_initial_state_graph(
              axes, lambda x: np.sin(x)
          )
          graph.set_stroke(width=4)
          graph_label = TexMobject(
              "T({x}, 0) = \\sin\\left({x}\\right)",
              tex_to_color_map=self.tex_to_color_map,
          )
          graph_label.next_to(
              graph.point_from_proportion(0.25), UR,
              buff=SMALL_BUFF,
          )
  
          v_line, x_tracker = self.get_v_line_with_x_tracker(graph)
  
          xs = VGroup(
              *graph_label.get_parts_by_tex("x"),
              axes.x_axis.label,
          )
  
          self.play(
              Write(axes),
              self.quick_sine_curve.become, graph,
              FadeOutAndShift(self.question_group, UP),
          )
          self.play(
              FadeInFromDown(graph_label),
              FadeIn(graph),
          )
          self.remove(self.quick_sine_curve)
          self.add(v_line)
          self.play(
              ApplyMethod(
                  x_tracker.set_value, TAU,
                  rate_func=lambda t: smooth(t, 3),
                  run_time=5,
              ),
              LaggedStartMap(
                  ShowCreationThenFadeAround, xs,
                  run_time=3,
                  lag_ratio=0.2,
              )
          )
          self.remove(v_line, x_tracker)
          self.wait()
  
          self.graph = graph
          self.graph_label = graph_label
          self.v_line = v_line
          self.x_tracker = x_tracker
  
      def reference_curvature(self):
          curve_segment, curve_x_tracker = \
              self.get_curve_segment_with_x_tracker(self.graph)
  
          self.add(curve_segment)
          self.play(
              curve_x_tracker.set_value, TAU,
              run_time=5,
              rate_func=lambda t: smooth(t, 3),
          )
          self.play(FadeOut(curve_segment))
  
          self.curve_segment = curve_segment
          self.curve_x_tracker = curve_x_tracker
  
      def show_derivatives(self):
          deriv1 = TexMobject(
              "{\\partial T \\over \\partial {x}}({x}, 0)",
              "= \\cos\\left({x}\\right)",
              tex_to_color_map=self.tex_to_color_map,
          )
          deriv2 = TexMobject(
              "{\\partial^2 T \\over \\partial {x}^2}({x}, 0)",
              "= -\\sin\\left({x}\\right)",
              tex_to_color_map=self.tex_to_color_map,
          )
  
          deriv1.to_corner(UR)
          deriv2.next_to(
              deriv1, DOWN,
              buff=0.75,
              aligned_edge=LEFT,
          )
          VGroup(deriv1, deriv2).shift(1.4 * RIGHT)
  
          self.play(
              Animation(Group(*self.get_mobjects())),
              FadeInFrom(deriv1, LEFT),
              self.camera.frame_center.shift, 2 * RIGHT,
          )
          self.wait()
          self.play(
              FadeInFrom(deriv2, UP)
          )
          self.wait()
  
          self.deriv1 = deriv1
          self.deriv2 = deriv2
  
      def show_curvature_matching_height(self):
          axes = self.axes
          graph = self.graph
          curve_segment = self.curve_segment
          curve_x_tracker = self.curve_x_tracker
  
          d2_graph = self.get_initial_state_graph(
              axes, lambda x: -np.sin(x),
          )
          dashed_d2_graph = DashedVMobject(d2_graph, num_dashes=50)
          dashed_d2_graph.color_using_background_image(None)
          dashed_d2_graph.set_stroke(RED, 2)
  
          vector, x_tracker = self.get_v_line_with_x_tracker(
              d2_graph,
              line_creator=lambda p1, p2: Arrow(
                  p1, p2, color=RED, buff=0
              )
          )
  
          lil_vectors = self.get_many_lil_vectors(graph)
          lil_vector = always_redraw(
              lambda: self.get_lil_vector(
                  graph, x_tracker.get_value()
              )
          )
  
          d2_rect = SurroundingRectangle(
              self.deriv2[-5:],
              color=RED,
          )
          self.play(ShowCreation(d2_rect))
          self.add(vector)
          self.add(lil_vector)
          self.add(curve_segment)
          curve_x_tracker.set_value(0)
          self.play(
              ShowCreation(dashed_d2_graph),
              x_tracker.set_value, TAU,
              curve_x_tracker.set_value, TAU,
              ShowIncreasingSubsets(lil_vectors[1:]),
              run_time=8,
              rate_func=linear,
          )
          self.remove(vector)
          self.remove(lil_vector)
          self.add(lil_vectors)
          self.play(
              FadeOut(curve_segment),
              FadeOut(d2_rect),
          )
  
          self.lil_vectors = lil_vectors
          self.dashed_d2_graph = dashed_d2_graph
  
      def show_time_step_scalings(self):
          axes = self.axes
          graph_label = self.graph_label
          dashed_d2_graph = self.dashed_d2_graph
          lil_vectors = self.lil_vectors
          graph = self.graph
  
          factor = 0.9
  
          new_label = TexMobject(
              "T({x}, \\Delta t) = c \\cdot \\sin\\left({x}\\right)",
              tex_to_color_map=self.tex_to_color_map,
          )
          final_label = TexMobject(
              "T({x}, {t}) = (\\text{something}) \\cdot \\sin\\left({x}\\right)",
              tex_to_color_map=self.tex_to_color_map,
          )
          for label in (new_label, final_label):
              label.shift(
                  graph_label.get_part_by_tex("=").get_center() -
                  label.get_part_by_tex("=").get_center()
              )
          final_label.shift(1.5 * LEFT)
  
          h_lines = VGroup(
              DashedLine(axes.c2p(0, 0, 1), axes.c2p(TAU, 0, 1)),
              DashedLine(axes.c2p(0, 0, -1), axes.c2p(TAU, 0, -1)),
          )
  
          lil_vectors.add_updater(lambda m: m.become(
              self.get_many_lil_vectors(graph)
          ))
  
          i = 4
          self.play(
              ReplacementTransform(
                  graph_label[:i], new_label[:i],
              ),
              ReplacementTransform(
                  graph_label[i + 1:i + 3],
                  new_label[i + 1:i + 3],
              ),
              FadeOutAndShift(graph_label[i], UP),
              FadeInFrom(new_label[i], DOWN),
          )
          self.play(
              ReplacementTransform(
                  graph_label[i + 3:],
                  new_label[i + 4:]
              ),
              FadeInFromDown(new_label[i + 3])
          )
          self.play(
              FadeOut(dashed_d2_graph),
              FadeIn(h_lines),
          )
          self.play(
              graph.stretch, factor, 1,
              h_lines.stretch, factor, 1,
          )
          self.wait()
  
          # Repeat
          last_coef = None
          last_exp = None
          delta_T = new_label.get_part_by_tex("\\Delta t")
          c = new_label.get_part_by_tex("c")[0]
          prefix = new_label[:4]
          prefix.generate_target()
          for x in range(5):
              coef = Integer(x + 2)
              exp = coef.copy().scale(0.7)
              coef.next_to(
                  delta_T, LEFT, SMALL_BUFF,
                  aligned_edge=DOWN,
              )
              exp.move_to(c.get_corner(UR), DL)
              anims1 = [FadeInFrom(coef, 0.25 * DOWN)]
              anims2 = [FadeInFrom(exp, 0.25 * DOWN)]
              if last_coef:
                  anims1.append(
                      FadeOutAndShift(last_coef, 0.25 * UP)
                  )
                  anims2.append(
                      FadeOutAndShift(last_exp, 0.25 * UP)
                  )
              last_coef = coef
              last_exp = exp
              prefix.target.next_to(coef, LEFT, SMALL_BUFF)
              prefix.target.match_y(prefix)
              anims1.append(MoveToTarget(prefix))
  
              self.play(*anims1)
              self.play(
                  graph.stretch, factor, 1,
                  h_lines.stretch, factor, 1,
                  *anims2,
              )
          self.play(
              ReplacementTransform(
                  new_label[:4],
                  final_label[:4],
              ),
              ReplacementTransform(
                  VGroup(last_coef, delta_T),
                  final_label.get_part_by_tex("{t}"),
              ),
              ReplacementTransform(
                  last_exp,
                  final_label.get_part_by_tex("something"),
              ),
              FadeOut(new_label.get_part_by_tex("\\cdot"), UP),
              ReplacementTransform(
                  new_label[-4:],
                  final_label[-4:],
              ),
              ReplacementTransform(
                  new_label.get_part_by_tex("="),
                  final_label.get_part_by_tex("="),
              ),
              ReplacementTransform(
                  new_label.get_part_by_tex(")"),
                  final_label.get_part_by_tex(")"),
              ),
          )
          final_label.add_background_rectangle(opacity=1)
          self.add(final_label)
          self.wait()
  
          group = VGroup(graph, h_lines)
          group.add_updater(lambda m, dt: m.stretch(
              (1 - 0.1 * dt), 1
          ))
          self.add(group)
          self.wait(10)
  
      def smooth_evolution(self):
          pass
  
      #
      def get_rod(self, temp_func):
          pass
  
      def get_v_line_with_x_tracker(self, graph, line_creator=DashedLine):
          axes = self.axes
          x_min = axes.x_axis.p2n(graph.get_start())
          x_max = axes.x_axis.p2n(graph.get_end())
          x_tracker = ValueTracker(x_min)
          get_x = x_tracker.get_value
          v_line = always_redraw(lambda: line_creator(
              axes.c2p(get_x(), 0, 0),
              graph.point_from_proportion(
                  inverse_interpolate(
                      x_min, x_max, get_x()
                  )
              ),
          ))
          return v_line, x_tracker
  
      def get_curve_segment_with_x_tracker(self, graph, delta_x=0.5):
          axes = self.axes
          x_min = axes.x_axis.p2n(graph.get_start())
          x_max = axes.x_axis.p2n(graph.get_end())
          x_tracker = ValueTracker(x_min)
          get_x = x_tracker.get_value
  
          def x2a(x):
              return inverse_interpolate(x_min, x_max, x)
  
          curve = VMobject(
              stroke_color=WHITE,
              stroke_width=5
          )
          curve.add_updater(lambda m: m.pointwise_become_partial(
              graph,
              max(x2a(get_x() - delta_x), 0),
              min(x2a(get_x() + delta_x), 1),
          ))
          return curve, x_tracker
  
      def get_lil_vector(self, graph, x):
          x_axis = self.axes.x_axis
          point = graph.point_from_proportion(x / TAU)
          x_axis_point = x_axis.n2p(x_axis.p2n(point))
          return Arrow(
              point,
              interpolate(
                  point, x_axis_point, 0.5,
              ),
              buff=0,
              color=RED
          )
  
      def get_many_lil_vectors(self, graph, n=13):
          return VGroup(*[
              self.get_lil_vector(graph, x)
              for x in np.linspace(0, TAU, n)
          ])
  
  
  class SineWaveScaledByExp(TemperatureGraphScene):
      CONFIG = {
          "axes_config": {
              "z_min": -1.5,
              "z_max": 1.5,
              "z_axis_config": {
                  "unit_size": 2,
                  "tick_frequency": 0.5,
                  "label_direction": LEFT,
              },
              "y_axis_config": {
                  "label_direction": RIGHT,
              },
          },
          "k": 0.3,
      }
  
      def construct(self):
          self.setup_axes()
          self.setup_camera()
          self.show_sine_wave()
          self.show_decay_surface()
          self.linger_at_end()
  
      def setup_axes(self):
          axes = self.get_three_d_axes()
  
          # Add number labels
          self.add_axes_numbers(axes)
          for axis in [axes.x_axis, axes.y_axis]:
              axis.numbers.rotate(
                  90 * DEGREES,
                  axis=axis.get_vector(),
                  about_point=axis.point_from_proportion(0.5)
              )
              axis.numbers.set_shade_in_3d(True)
          axes.z_axis.add_numbers(*range(-1, 2))
          for number in axes.z_axis.numbers:
              number.rotate(90 * DEGREES, RIGHT)
  
          axes.z_axis.label.next_to(
              axes.z_axis.get_end(), OUT,
          )
  
          # Input plane
          axes.input_plane.set_opacity(0.25)
          self.add(axes.input_plane)
  
          # Shift into place
          # axes.shift(5 * LEFT)
          self.axes = axes
          self.add(axes)
  
      def setup_camera(self):
          self.set_camera_orientation(
              phi=80 * DEGREES,
              theta=-80 * DEGREES,
              distance=50,
          )
          self.camera.set_frame_center(
              2 * RIGHT,
          )
  
      def show_sine_wave(self):
          time_tracker = ValueTracker(0)
          graph = always_redraw(
              lambda: self.get_time_slice_graph(
                  self.axes,
                  self.sin_exp,
                  t=time_tracker.get_value(),
              )
          )
          graph.suspend_updating()
  
          graph_label = TexMobject("\\sin(x)")
          graph_label.set_color(BLUE)
          graph_label.rotate(90 * DEGREES, RIGHT)
          graph_label.next_to(
              graph.point_from_proportion(0.25),
              OUT,
              SMALL_BUFF,
          )
  
          self.play(
              ShowCreation(graph),
              FadeInFrom(graph_label, IN)
          )
          self.wait()
          graph.resume_updating()
  
          self.time_tracker = time_tracker
          self.graph = graph
  
      def show_decay_surface(self):
          time_tracker = self.time_tracker
          axes = self.axes
  
          plane = Rectangle()
          plane.rotate(90 * DEGREES, RIGHT)
          plane.set_stroke(width=0)
          plane.set_fill(WHITE, 0.2)
          plane.match_depth(axes.z_axis)
          plane.match_width(axes.x_axis, stretch=True)
          plane.add_updater(
              lambda p: p.move_to(axes.c2p(
                  0,
                  time_tracker.get_value(),
                  0,
              ), LEFT)
          )
  
          time_slices = VGroup(*[
              self.get_time_slice_graph(
                  self.axes,
                  self.sin_exp,
                  t=t,
              )
              for t in range(0, 10)
          ])
          surface_t_tracker = ValueTracker(0)
          surface = always_redraw(
              lambda: self.get_surface(
                  self.axes,
                  self.sin_exp,
                  v_max=surface_t_tracker.get_value(),
              ).set_stroke(opacity=0)
          )
  
          exp_graph = ParametricFunction(
              lambda t: axes.c2p(
                  PI / 2,
                  t,
                  self.sin_exp(PI / 2, t)
              ),
              t_min=axes.y_min,
              t_max=axes.y_max,
          )
          exp_graph.set_stroke(RED, 3)
          exp_graph.set_shade_in_3d(True)
  
          exp_label = TexMobject("e^{-\\alpha t}")
          exp_label.scale(1.5)
          exp_label.set_color(RED)
          exp_label.rotate(90 * DEGREES, RIGHT)
          exp_label.rotate(90 * DEGREES, OUT)
          exp_label.next_to(
              exp_graph.point_from_proportion(0.3),
              OUT + UP,
          )
  
          self.move_camera(
              theta=-25 * DEGREES,
          )
          self.add(surface)
          self.add(plane)
          self.play(
              surface_t_tracker.set_value, axes.y_max,
              time_tracker.set_value, axes.y_max,
              ShowIncreasingSubsets(
                  time_slices,
                  int_func=np.ceil,
              ),
              run_time=5,
              rate_func=linear,
          )
          surface.clear_updaters()
  
          self.play(
              ShowCreation(exp_graph),
              FadeOut(plane),
              FadeInFrom(exp_label, IN),
              time_slices.set_stroke, {"width": 1},
          )
  
      def linger_at_end(self):
          self.wait()
          self.begin_ambient_camera_rotation(rate=-0.02)
          self.wait(20)
  
      #
      def sin_exp(self, x, t):
          return np.sin(x) * np.exp(-self.k * t)
  
  
  class BoundaryConditionReference(ShowEvolvingTempGraphWithArrows):
      def construct(self):
          self.setup_axes()
          self.setup_graph()
  
          rod = self.get_rod(0, 10)
          self.color_rod_by_graph(rod)
  
          boundary_points = [
              rod.get_right(),
              rod.get_left(),
          ]
          boundary_dots = VGroup(*[
              Dot(point, radius=0.2)
              for point in boundary_points
          ])
          boundary_arrows = VGroup(*[
              Vector(2 * DOWN).next_to(dot, UP)
              for dot in boundary_dots
          ])
          boundary_arrows.set_stroke(YELLOW, 10)
  
          words = TextMobject(
              "Different rules\\\\",
              "at the boundary",
          )
          words.scale(1.5)
          words.to_edge(UP)
  
          # self.add(self.axes)
          # self.add(self.graph)
          self.add(rod)
          self.play(FadeInFromDown(words))
          self.play(
              LaggedStartMap(GrowArrow, boundary_arrows),
              LaggedStartMap(GrowFromCenter, boundary_dots),
              lag_ratio=0.3,
              run_time=1,
          )
          self.wait()
  
  
  class SimulateRealSineCurve(ShowEvolvingTempGraphWithArrows):
      CONFIG = {
          "axes_config": {
              "x_min": 0,
              "x_max": TAU,
              "x_axis_config": {
                  "unit_size": 1.5,
                  "include_tip": False,
                  "tick_frequency": PI / 4,
              },
              "y_min": -1.5,
              "y_max": 1.5,
              "y_axis_config": {
                  "tick_frequency": 0.5,
                  "unit_size": 2,
              },
          },
          "graph_x_min": 0,
          "graph_x_max": TAU,
          "arrow_xs": np.linspace(0, TAU, 13),
          "rod_opacity": 0.5,
          "wait_time": 30,
          "alpha": 0.5,
      }
  
      def construct(self):
          self.add_axes()
          self.add_graph()
          self.add_clock()
          self.add_rod()
          self.add_arrows()
          self.initialize_updaters()
          self.let_play()
  
      def add_labels_to_axes(self):
          x_axis = self.axes.x_axis
          x_axis.add(*[
              TexMobject(tex).scale(0.5).next_to(
                  x_axis.n2p(n),
                  DOWN,
                  buff=MED_SMALL_BUFF
              )
              for tex, n in [
                  ("\\tau \\over 4", TAU / 4),
                  ("\\tau \\over 2", TAU / 2),
                  ("3 \\tau \\over 4", 3 * TAU / 4),
                  ("\\tau", TAU),
              ]
          ])
  
      def add_axes(self):
          super().add_axes()
          self.add_labels_to_axes()
  
      def add_rod(self):
          super().add_rod()
          self.rod.set_opacity(self.rod_opacity)
          self.rod.set_stroke(width=0)
  
      def initial_function(self, x):
          return np.sin(x)
  
      def y_to_color(self, y):
          return temperature_to_color(0.8 * y)
  
  
  class StraightLine3DGraph(TemperatureGraphScene):
      CONFIG = {
          "axes_config": {
              "z_min": 0,
              "z_max": 10,
              "z_axis_config": {
                  'unit_size': 0.5,
              }
          },
          "c": 1.2,
      }
  
      def construct(self):
          axes = self.get_three_d_axes()
          axes.add(axes.input_plane)
          axes.move_to(2 * IN + UP, IN)
          surface = self.get_surface(
              axes, self.func,
          )
          initial_graph = self.get_initial_state_graph(
              axes, lambda x: self.func(x, 0)
          )
  
          plane = self.get_const_time_plane(axes)
          initial_graph.add_updater(
              lambda m: m.move_to(plane, IN)
          )
  
          self.set_camera_orientation(
              phi=80 * DEGREES,
              theta=-100 * DEGREES,
          )
          self.begin_ambient_camera_rotation()
  
          self.add(axes)
          self.add(initial_graph)
          self.play(
              TransformFromCopy(initial_graph, surface)
          )
          self.add(surface, initial_graph)
          self.wait()
  
          self.play(
              FadeIn(plane),
              ApplyMethod(
                  plane.t_tracker.set_value, 10,
                  rate_func=linear,
                  run_time=10,
              )
          )
          self.play(FadeOut(plane))
          self.wait(15)
  
      def func(self, x, t):
          return self.c * x
  
  
  class SimulateLinearGraph(SimulateRealSineCurve):
      CONFIG = {
          "axes_config": {
              "y_min": 0,
              "y_max": 3,
              "y_axis_config": {
                  "tick_frequency": 0.5,
                  "unit_size": 2,
              },
          },
          "arrow_scale_factor": 2,
          "alpha": 1,
          "wait_time": 40,
          "step_size": 0.02,
      }
  
      # def let_play(self):
      #     pass
  
      def add_labels_to_axes(self):
          pass
  
      def y_to_color(self, y):
          return temperature_to_color(0.8 * (y - 1.5))
  
      def initial_function(self, x):
          axes = self.axes
          y_max = axes.y_max
          x_max = axes.x_max
          slope = y_max / x_max
          return slope * x
  
  
  class EmphasizeBoundaryPoints(SimulateLinearGraph):
      CONFIG = {
          "wait_time": 30,
      }
  
      def let_play(self):
          rod = self.rod
          self.graph.update(0.01)
          self.arrows.update()
  
          to_update = VGroup(
              self.graph,
              self.arrows,
              self.time_label,
          )
          for mob in to_update:
              mob.suspend_updating()
  
          dots = VGroup(
              Dot(rod.get_left()),
              Dot(rod.get_right()),
          )
          for dot in dots:
              dot.set_height(0.2)
              dot.set_color(YELLOW)
  
          words = TextMobject(
              "Different rules\\\\"
              "at the boundary"
          )
          words.next_to(rod, UP)
  
          arrows = VGroup(*[
              Arrow(
                  words.get_corner(corner),
                  dot.get_top(),
                  path_arc=u * 60 * DEGREES,
              )
              for corner, dot, u in zip(
                  [LEFT, RIGHT], dots, [1, -1]
              )
          ])
  
          self.add(words)
          self.play(
              LaggedStartMap(
                  FadeInFromLarge, dots,
                  scale_factor=5,
              ),
              LaggedStartMap(
                  ShowCreation, arrows,
              ),
              lag_ratio=0.4
          )
          self.wait()
  
          for mob in to_update:
              mob.resume_updating()
  
          super().let_play()
  
  
  class FlatEdgesContinuousEvolution(ShowEvolvingTempGraphWithArrows):
      CONFIG = {
          "wait_time": 30,
          "freq_amplitude_pairs": [
              (1, 0.5),
              (2, 1),
              (3, 0.5),
              (4, 0.3),
          ],
      }
  
      def construct(self):
          self.add_axes()
          self.add_graph()
          self.add_clock()
          self.add_rod()
          self.initialize_updaters()
          self.add_boundary_lines()
          self.let_play()
  
      def add_boundary_lines(self):
  
          lines = VGroup(*[
              Line(LEFT, RIGHT)
              for x in range(2)
          ])
          lines.set_width(1.5)
          lines.set_stroke(WHITE, 5, opacity=0.5)
          lines.add_updater(self.update_lines)
  
          turn_animation_into_updater(
              ShowCreation(lines, run_time=2)
          )
          self.add(lines)
  
      def update_lines(self, lines):
          graph = self.graph
          lines[0].move_to(graph.get_start())
          lines[1].move_to(graph.get_end())
  
      def initial_function(self, x):
          return ShowEvolvingTempGraphWithArrows.initial_function(self, x)
  
  
  class MoreAccurateTempFlowWithArrows(ShowEvolvingTempGraphWithArrows):
      CONFIG = {
          "arrow_scale_factor": 1,
          "max_magnitude": 1,
          "freq_amplitude_pairs": [
              (1, 0.5),
              (2, 1),
              (3, 0.5),
              (4, 0.3),
          ],
      }
  
      def setup_axes(self):
          super().setup_axes()
          y_axis = self.axes.y_axis
          y_axis.remove(y_axis.label)
  
  
  class SlopeToHeatFlow(FlatEdgesContinuousEvolution):
      CONFIG = {
          "wait_time": 20,
          "xs": [1.75, 5.25, 7.8],
          "axes_config": {
              "x_min": 0,
              "x_max": 10,
              "x_axis_config": {
                  "include_tip": False,
              }
          },
          "n_arrows": 10,
          "random_seed": 1,
      }
  
      def construct(self):
          self.add_axes()
          self.add_graph()
          self.add_rod()
          #
          self.show_slope_labels()
          self.show_heat_flow()
  
      def show_slope_labels(self):
          axes = self.axes
          # axes.x_axis.add_numbers()
          graph = self.graph
          xs = self.xs
  
          lines = VGroup(*[
              TangentLine(
                  graph,
                  inverse_interpolate(
                      axes.x_min,
                      axes.x_max,
                      x
                  ),
                  length=2,
                  stroke_opacity=0.75,
              )
              for x in xs
          ])
  
          slope_words = VGroup()
          for line in lines:
              slope = line.get_slope()
              word = TextMobject(
                  "Slope =",
                  "${:.2}$".format(slope)
              )
              if slope > 0:
                  word[1].set_color(GREEN)
              else:
                  word[1].set_color(RED)
  
              word.set_width(line.get_length())
              word.next_to(ORIGIN, UP, SMALL_BUFF)
              word.rotate(
                  line.get_angle(),
                  about_point=ORIGIN,
              )
              word.shift(line.get_center())
              slope_words.add(word)
  
          self.play(
              LaggedStartMap(ShowCreation, lines),
              LaggedStartMap(Write, slope_words),
              lag_ratio=0.5,
              run_time=1,
          )
          self.wait()
  
          self.lines = lines
          self.slope_words = slope_words
  
      def show_heat_flow(self):
          axes = self.axes
          xs = self.xs
          slope_words = self.slope_words
          tan_lines = self.lines
  
          slopes = map(Line.get_slope, self.lines)
          flow_rates = [-s for s in slopes]
  
          points = list(map(axes.x_axis.n2p, xs))
          v_lines = VGroup(*[
              Line(
                  line.get_center(), point,
                  stroke_width=1,
              )
              for point, line in zip(points, tan_lines)
          ])
  
          flow_words = VGroup(*[
              TextMobject("Heat flow").next_to(
                  point, DOWN
              ).scale(0.8)
              for point in points
          ])
          flow_mobjects = VGroup(*[
              self.get_flow(x, flow_rate)
              for x, flow_rate in zip(xs, flow_rates)
          ])
  
          self.add(flow_mobjects)
          self.wait()
          self.play(
              LaggedStart(*[
                  TransformFromCopy(
                      sw[0], fw[0]
                  )
                  for sw, fw in zip(slope_words, flow_words)
              ]),
              LaggedStartMap(ShowCreation, v_lines),
              lag_ratio=0.4,
              run_time=2,
          )
          self.wait(self.wait_time)
  
      def get_flow(self, x, flow_rate):
          return VGroup(*[
              self.get_single_flow_arrow(
                  x, flow_rate,
                  t_offset=to
              )
              for to in np.linspace(0, 5, self.n_arrows)
          ])
  
      def get_single_flow_arrow(self, x, flow_rate, t_offset):
          axes = self.axes
          point = axes.x_axis.n2p(x)
  
          h_shift = 0.5
          v_shift = 0.4 * np.random.random() + 0.1
  
          arrow = Vector(0.5 * RIGHT * np.sign(flow_rate))
          arrow.move_to(point)
          arrow.shift(v_shift * UP)
          lp = arrow.get_center() + h_shift * LEFT
          rp = arrow.get_center() + h_shift * RIGHT
          lc = self.rod_point_to_color(lp)
          rc = self.rod_point_to_color(rp)
  
          run_time = 3 / flow_rate
          animation = UpdateFromAlphaFunc(
              arrow,
              lambda m, a: m.move_to(
                  interpolate(lp, rp, a)
              ).set_color(
                  interpolate_color(lc, rc, a)
              ).set_opacity(there_and_back(a)),
              run_time=run_time,
          )
          result = cycle_animation(animation)
          animation.total_time += t_offset
          return result
  
  
  class CloserLookAtStraightLine(SimulateLinearGraph):
      def construct(self):
          self.add_axes()
          self.add_graph()
          self.add_clock()
          self.add_rod()
          # self.initialize_updaters()
          #
          self.show_t_eq_0_state()
          self.show_t_gt_0_state()
  
      def show_t_eq_0_state(self):
          t_eq = TexMobject("t", "=", "0")
          t_eq.next_to(self.time_label, DOWN)
  
          circles = VGroup(*[
              Circle(
                  radius=0.25,
                  stroke_color=YELLOW,
              )
              for x in range(2)
          ])
          circles.add_updater(self.attach_to_endpoints)
  
          self.play(Write(t_eq))
          self.play(ShowCreation(circles))
          self.wait()
          self.play(FadeOut(circles))
  
          self.circles = circles
          self.t_eq = t_eq
  
      def show_t_gt_0_state(self):
          # circles = self.circles
          t_eq = self.t_eq
  
          t_ineq = TexMobject("t", ">", "0")
          t_ineq.move_to(t_eq)
  
          slope_lines = VGroup(*[
              Line(LEFT, RIGHT)
              for x in range(2)
          ])
          slope_lines.set_opacity(0.5)
          slope_lines.add_updater(self.attach_to_endpoints)
  
          self.remove(t_eq)
          self.add(t_ineq)
  
          self.initialize_updaters()
          self.run_clock(0.1)
          for mob in self.mobjects:
              mob.suspend_updating()
          self.wait()
  
          self.add(slope_lines)
          self.add(self.clock, self.time_label, t_ineq)
          self.play(ShowCreation(slope_lines))
          for mob in self.mobjects:
              mob.resume_updating()
  
          self.run_clock(self.wait_time)
  
      #
      def attach_to_endpoints(self, mobs):
          points = [
              self.graph.get_start(),
              self.graph.get_end(),
          ]
          for mob, point in zip(mobs, points):
              mob.move_to(point)
          return mobs
  
  
  class ManipulateSinExpSurface(TemperatureGraphScene):
      CONFIG = {
          "axes_config": {
              "z_max": 1.25,
              "z_min": -1.25,
              "z_axis_config": {
                  "unit_size": 2.5,
                  "tick_frequency": 0.5,
              },
              "x_axis_config": {
                  # "unit_size": 1.5,
                  "unit_size": 1.0,
                  "tick_frequency": 1,
              },
              "x_max": 10,
              "y_max": 15,
          },
          "alpha": 0.2,
          "tex_mobject_config": {
              "tex_to_color_map": {
                  "{x}": GREEN,
                  "{t}": YELLOW,
                  "\\omega": MAROON_B,
                  "^2": WHITE,
              },
          },
          "graph_config": {},
          "initial_phi": -90 * DEGREES,
          "initial_omega": 1,
      }
  
      def construct(self):
          self.setup_axes()
          self.add_sine_wave()
          self.shift_sine_to_cosine()
          self.show_derivatives_of_cos()
          self.show_cos_exp_surface()
          self.change_frequency()
          self.talk_through_omega()
          self.show_cos_omega_derivatives()
          self.show_rebalanced_exp()
  
      def setup_axes(self):
          axes = self.get_three_d_axes(include_numbers=True)
          axes.x_axis.remove(axes.x_axis.numbers)
          L = TexMobject("L")
          L.rotate(90 * DEGREES, RIGHT)
          L.next_to(axes.x_axis.get_end(), IN)
          axes.x_axis.label = L
          axes.x_axis.add(L)
  
          axes.shift(5 * LEFT + 0.5 * IN)
          axes.z_axis.label[0].remove(
              *axes.z_axis.label[0][1:]
          )
          axes.z_axis.label.next_to(
              axes.z_axis.get_end(), OUT
          )
          axes.z_axis.add_numbers(
              *np.arange(-1, 1.5, 0.5),
              direction=LEFT,
              number_config={
                  "num_decimal_places": 1,
              }
          )
          for number in axes.z_axis.numbers:
              number.rotate(90 * DEGREES, RIGHT)
  
          self.set_camera_orientation(
              phi=90 * DEGREES,
              theta=-90 * DEGREES,
              distance=100,
          )
  
          self.add(axes)
          self.remove(axes.y_axis)
          self.axes = axes
  
      def add_sine_wave(self):
          self.initialize_parameter_trackers()
          graph = self.get_graph()
          sin_label = TexMobject(
              "\\sin\\left({x}\\right)",
              **self.tex_mobject_config,
          )
          sin_label.shift(2 * LEFT + 2.75 * UP)
  
          self.add_fixed_in_frame_mobjects(sin_label)
          graph.suspend_updating()
          self.play(
              ShowCreation(graph),
              Write(sin_label),
          )
          graph.resume_updating()
          self.wait()
  
          self.sin_label = sin_label
          self.graph = graph
  
      def shift_sine_to_cosine(self):
          graph = self.graph
          sin_label = self.sin_label
  
          sin_cross = Cross(sin_label)
          sin_cross.add_updater(
              lambda m: m.move_to(sin_label)
          )
          cos_label = TexMobject(
              "\\cos\\left({x}\\right)",
              **self.tex_mobject_config,
          )
          cos_label.move_to(sin_label, LEFT)
          cos_label.shift(LEFT)
          # cos_label.shift(
          #     axes.c2p(0, 0) - axes.c2p(PI / 2, 0),
          # )
  
          left_tangent = Line(ORIGIN, RIGHT)
          left_tangent.set_stroke(WHITE, 5)
  
          self.add_fixed_in_frame_mobjects(cos_label)
          self.play(
              ApplyMethod(
                  self.phi_tracker.set_value, 0,
              ),
              FadeOutAndShift(sin_label, LEFT),
              FadeInFrom(cos_label, RIGHT),
              run_time=2,
          )
          left_tangent.move_to(graph.get_start(), LEFT)
          self.play(ShowCreation(left_tangent))
          self.play(FadeOut(left_tangent))
  
          self.cos_label = cos_label
  
      def show_derivatives_of_cos(self):
          cos_label = self.cos_label
          cos_exp_label = TexMobject(
              "\\cos\\left({x}\\right)",
              "e^{-\\alpha {t}}",
              **self.tex_mobject_config
          )
          cos_exp_label.move_to(cos_label, LEFT)
  
          ddx_group, ddx_exp_group = [
              self.get_ddx_computation_group(
                  label,
                  *[
                      TexMobject(
                          s + "\\left({x}\\right)" + exp,
                          **self.tex_mobject_config,
                      )
                      for s in ["-\\sin", "-\\cos"]
                  ]
              )
              for label, exp in [
                  (cos_label, ""),
                  (cos_exp_label, "e^{-\\alpha {t}}"),
              ]
          ]
  
          self.add_fixed_in_frame_mobjects(ddx_group)
          self.play(FadeIn(ddx_group))
          self.wait()
  
          # Cos exp
          transforms = [
              ReplacementTransform(
                  cos_label, cos_exp_label,
              ),
              ReplacementTransform(
                  ddx_group, ddx_exp_group,
              ),
          ]
          for trans in transforms:
              trans.begin()
              self.add_fixed_in_frame_mobjects(trans.mobject)
          self.play(*transforms)
          self.add_fixed_in_frame_mobjects(
              cos_exp_label, ddx_exp_group
          )
          self.remove_fixed_in_frame_mobjects(
              cos_label,
              *[
                  trans.mobject
                  for trans in transforms
              ]
          )
  
          self.cos_exp_label = cos_exp_label
          self.ddx_exp_group = ddx_exp_group
  
      def show_cos_exp_surface(self):
          axes = self.axes
  
          surface = self.get_cos_exp_surface()
          self.add(surface)
          surface.max_t_tracker.set_value(0)
          self.move_camera(
              phi=85 * DEGREES,
              theta=-80 * DEGREES,
              added_anims=[
                  ApplyMethod(
                      surface.max_t_tracker.set_value,
                      axes.y_max,
                      run_time=4,
                  ),
                  Write(axes.y_axis),
              ]
          )
          self.wait()
  
          self.surface = surface
  
      def change_frequency(self):
          cos_exp_label = self.cos_exp_label
          ddx_exp_group = self.ddx_exp_group
          omega_tracker = self.omega_tracker
  
          cos_omega = TexMobject(
              "\\cos\\left(",
              "\\omega", "\\cdot", "{x}",
              "\\right)",
              **self.tex_mobject_config
          )
          cos_omega.move_to(cos_exp_label, LEFT)
  
          omega = cos_omega.get_part_by_tex("\\omega")
          brace = Brace(omega, UP, buff=SMALL_BUFF)
          omega_decimal = always_redraw(
              lambda: DecimalNumber(
                  omega_tracker.get_value(),
                  color=omega.get_color(),
              ).next_to(brace, UP, SMALL_BUFF)
          )
  
          self.add_fixed_in_frame_mobjects(
              cos_omega,
              brace,
              omega_decimal,
          )
          self.play(
              self.camera.phi_tracker.set_value, 90 * DEGREES,
              self.camera.theta_tracker.set_value, -90 * DEGREES,
              FadeOut(self.surface),
              FadeOut(self.axes.y_axis),
              FadeOut(cos_exp_label),
              FadeOut(ddx_exp_group),
              FadeIn(cos_omega),
              GrowFromCenter(brace),
              FadeInFromDown(omega_decimal)
          )
          for n in [2, 6, 1, 4]:
              freq = n * PI / self.axes.x_max
              self.play(
                  omega_tracker.set_value, freq,
                  run_time=2
              )
              self.wait()
          self.wait()
  
          self.cos_omega = cos_omega
          self.omega_brace = brace
  
      def talk_through_omega(self):
          axes = self.axes
  
          x_tracker = ValueTracker(0)
          get_x = x_tracker.get_value
          v_line = always_redraw(lambda: DashedLine(
              axes.c2p(get_x(), 0, 0),
              axes.c2p(get_x(), 0, self.func(get_x(), 0)),
          ))
  
          x = self.cos_omega.get_part_by_tex("{x}")
          brace = Brace(x, DOWN)
          x_decimal = always_redraw(
              lambda: DecimalNumber(
                  get_x(),
                  color=x.get_color()
              ).next_to(brace, DOWN, SMALL_BUFF)
          )
  
          self.add(v_line)
          self.add_fixed_in_frame_mobjects(brace, x_decimal)
          self.play(
              x_tracker.set_value, 5,
              run_time=5,
              rate_func=linear,
          )
          self.play(
              FadeOut(v_line),
              FadeOut(brace),
              FadeOut(x_decimal),
          )
          self.remove_fixed_in_frame_mobjects(
              brace, x_decimal
          )
  
      def show_cos_omega_derivatives(self):
          cos_omega = self.cos_omega
          ddx_omega_group = self.get_ddx_computation_group(
              cos_omega,
              *[
                  TexMobject(
                      s + "\\left(\\omega \\cdot {x}\\right)",
                      **self.tex_mobject_config,
                  )
                  for s in ["-\\omega \\sin", "-\\omega^2 \\cos"]
              ]
          )
  
          omega_squared = ddx_omega_group[-1][1:3]
          rect = SurroundingRectangle(omega_squared)
  
          self.add_fixed_in_frame_mobjects(ddx_omega_group)
          self.play(FadeIn(ddx_omega_group))
          self.wait()
          self.add_fixed_in_frame_mobjects(rect)
          self.play(ShowCreationThenFadeOut(rect))
          self.wait()
  
          self.ddx_omega_group = ddx_omega_group
  
      def show_rebalanced_exp(self):
          cos_omega = self.cos_omega
          ddx_omega_group = self.ddx_omega_group
  
          cos_exp = TexMobject(
              "\\cos\\left(",
              "\\omega", "\\cdot", "{x}",
              "\\right)",
              "e^{-\\alpha \\omega^2 {t}}",
              **self.tex_mobject_config
          )
          cos_exp.move_to(cos_omega, DL)
  
          self.add_fixed_in_frame_mobjects(cos_exp)
          self.play(
              FadeOut(cos_omega),
              FadeOut(ddx_omega_group),
              FadeIn(cos_exp),
          )
          self.remove_fixed_in_frame_mobjects(
              cos_omega,
              ddx_omega_group,
          )
  
          self.play(
              FadeIn(self.surface),
              FadeIn(self.axes.y_axis),
              VGroup(
                  cos_exp,
                  self.omega_brace,
              ).shift, 4 * RIGHT,
              self.camera.phi_tracker.set_value, 80 * DEGREES,
              self.camera.theta_tracker.set_value, -80 * DEGREES,
          )
          self.wait()
          self.play(
              self.omega_tracker.set_value, TAU / 10,
              run_time=6,
          )
  
      #
      def initialize_parameter_trackers(self):
          self.phi_tracker = ValueTracker(
              self.initial_phi
          )
          self.omega_tracker = ValueTracker(
              self.initial_omega
          )
          self.t_tracker = ValueTracker(0)
  
      def get_graph(self):
          return always_redraw(
              lambda: self.get_time_slice_graph(
                  self.axes, self.func,
                  t=self.t_tracker.get_value(),
                  **self.graph_config
              )
          )
  
      def get_cos_exp_surface(self):
          max_t_tracker = ValueTracker(self.axes.y_max)
          surface = always_redraw(
              lambda: self.get_surface(
                  self.axes,
                  self.func,
                  v_max=max_t_tracker.get_value(),
              )
          )
          surface.max_t_tracker = max_t_tracker
          return surface
  
      def func(self, x, t):
          phi = self.phi_tracker.get_value()
          omega = self.omega_tracker.get_value()
          alpha = self.alpha
          return op.mul(
              np.cos(omega * (x + phi)),
              np.exp(-alpha * (omega**2) * t)
          )
  
      def get_ddx_computation_group(self, f, df, ddf):
          arrows = VGroup(*[
              Vector(0.5 * RIGHT) for x in range(2)
          ])
          group = VGroup(
              arrows[0], df, arrows[1], ddf
          )
          group.arrange(RIGHT)
          group.next_to(f, RIGHT)
  
          for arrow in arrows:
              label = TexMobject(
                  "\\partial \\over \\partial {x}",
                  **self.tex_mobject_config,
              )
              label.scale(0.5)
              label.next_to(arrow, UP, SMALL_BUFF)
              arrow.add(label)
  
          group.arrows = arrows
          group.funcs = VGroup(df, ddf)
  
          return group
  
  
  class ShowFreq1CosExpDecay(ManipulateSinExpSurface):
      CONFIG = {
          "freq": 1,
          "alpha": 0.2,
      }
  
      def construct(self):
          self.setup_axes()
          self.add_back_y_axis()
          self.initialize_parameter_trackers()
          self.phi_tracker.set_value(0)
          self.omega_tracker.set_value(
              self.freq * TAU / 10,
          )
          #
          self.show_decay()
  
      def add_back_y_axis(self):
          axes = self.axes
          self.add(axes.y_axis)
          self.remove(axes.y_axis.numbers)
          self.remove(axes.y_axis.label)
  
      def show_decay(self):
          axes = self.axes
          t_tracker = self.t_tracker
          t_max = self.axes.y_max
  
          graph = always_redraw(
              lambda: self.get_time_slice_graph(
                  axes,
                  self.func,
                  t=t_tracker.get_value(),
                  stroke_width=5,
              )
          )
  
          surface = self.get_surface(
              self.axes, self.func,
          )
          plane = self.get_const_time_plane(axes)
  
          self.add(surface, plane, graph)
  
          self.set_camera_orientation(
              phi=80 * DEGREES,
              theta=-85 * DEGREES,
          )
          self.begin_ambient_camera_rotation(rate=0.01)
          self.play(
              t_tracker.set_value, t_max,
              plane.t_tracker.set_value, t_max,
              run_time=t_max,
              rate_func=linear,
          )
  
  
  class ShowFreq2CosExpDecay(ShowFreq1CosExpDecay):
      CONFIG = {
          "freq": 2,
      }
  
  
  class ShowFreq4CosExpDecay(ShowFreq1CosExpDecay):
      CONFIG = {
          "freq": 4,
      }
  
  
  class ShowHarmonics(SimulateRealSineCurve):
      CONFIG = {
          "rod_opacity": 0.75,
          "initial_omega": 1.27,
          "default_n_rod_pieces": 32,
      }
  
      def construct(self):
          self.add_axes()
          self.add_graph()
          self.add_rod()
          self.rod.add_updater(self.color_rod_by_graph)
          #
          self.show_formula()
  
      def add_graph(self):
          omega_tracker = ValueTracker(self.initial_omega)
          get_omega = omega_tracker.get_value
          graph = always_redraw(
              lambda: self.axes.get_graph(
                  lambda x: np.cos(get_omega() * x),
                  x_min=self.graph_x_min,
                  x_max=self.graph_x_max,
                  step_size=self.step_size,
                  discontinuities=[5],
              ).color_using_background_image("VerticalTempGradient")
          )
  
          self.add(graph)
          self.graph = graph
          self.omega_tracker = omega_tracker
  
      def show_formula(self):
          rod = self.rod
          graph = self.graph
          axes = self.axes
          omega_tracker = self.omega_tracker
          L = TAU
  
          formula = TexMobject(
              "=", "\\cos\\left(",
              "2(\\pi / L)", "\\cdot", "{x}",
              "\\right)",
              tex_to_color_map={
                  "{x}": GREEN,
              }
          )
          formula.next_to(
              self.axes.y_axis.label, RIGHT, SMALL_BUFF
          )
          omega_part = formula.get_part_by_tex("\\pi")
          omega_brace = Brace(omega_part, DOWN)
          omega = TexMobject("\\omega")
          omega.set_color(MAROON_B)
          omega.next_to(omega_brace, DOWN, SMALL_BUFF)
          formula.remove(omega_part)
  
          pi_over_L = TexMobject("(\\pi / L)")
          pi_over_L.move_to(omega_part)
          pi_over_L.match_color(omega)
  
          self.add(formula)
          self.add(omega_brace)
          self.add(omega)
  
          self.remove(graph)
          self.play(GrowFromEdge(rod, LEFT))
          self.play(
              ShowCreationThenFadeAround(axes.x_axis.label)
          )
          self.wait()
          self.play(FadeIn(graph))
          self.play(FadeInFromDown(pi_over_L))
          self.play(
              omega_tracker.set_value, PI / L,
              run_time=2
          )
          self.wait()
  
          # Show x
          x_tracker = ValueTracker(0)
          tip = ArrowTip(
              start_angle=-90 * DEGREES,
              color=WHITE,
          )
          tip.add_updater(lambda m: m.move_to(
              axes.x_axis.n2p(x_tracker.get_value()),
              DOWN,
          ))
          x_sym = TexMobject("x")
          x_sym.set_color(GREEN)
          x_sym.add_background_rectangle(buff=SMALL_BUFF)
          x_sym.add_updater(lambda m: m.next_to(tip, UP, SMALL_BUFF))
  
          self.play(
              Write(tip),
              Write(x_sym),
          )
          self.play(
              x_tracker.set_value, L,
              run_time=3,
          )
          self.wait()
          self.play(TransformFromCopy(
              x_sym, formula.get_part_by_tex("{x}")
          ))
          self.play(
              FadeOut(tip),
              FadeOut(x_sym),
          )
  
          # Harmonics
          pi_over_L.generate_target()
          n_sym = Integer(2)
          n_sym.match_color(pi_over_L)
          group = VGroup(n_sym, pi_over_L.target)
          group.arrange(RIGHT, buff=SMALL_BUFF)
          group.move_to(pi_over_L)
  
          self.play(
              MoveToTarget(pi_over_L),
              FadeInFromDown(n_sym),
              ApplyMethod(
                  omega_tracker.set_value, 2 * PI / L,
                  run_time=2,
              )
          )
          self.wait()
          for n in [*range(3, 9), 0]:
              new_n_sym = Integer(n)
              new_n_sym.move_to(n_sym, DR)
              new_n_sym.match_style(n_sym)
              self.play(
                  FadeOutAndShift(n_sym, UP),
                  FadeInFrom(new_n_sym, DOWN),
                  omega_tracker.set_value, n * PI / L,
              )
              self.wait()
              n_sym = new_n_sym
  
      #
      def add_labels_to_axes(self):
          x_axis = self.axes.x_axis
          L = TexMobject("L")
          L.next_to(x_axis.get_end(), DOWN)
          x_axis.add(L)
          x_axis.label = L
  
  
  class ShowHarmonicSurfaces(ManipulateSinExpSurface):
      CONFIG = {
          "alpha": 0.2,
          "initial_phi": 0,
          "initial_omega": PI / 10,
          "n_iterations": 8,
          "default_surface_config": {
              "resolution": (40, 30),
              "surface_piece_config": {
                  "stroke_width": 0.5,
              }
          }
      }
  
      def construct(self):
          self.setup_axes()
          self.initialize_parameter_trackers()
          self.add_surface()
          self.add_graph()
          self.show_all_harmonic_surfaces()
  
      def setup_axes(self):
          super().setup_axes()
          self.add(self.axes.y_axis)
          self.set_camera_orientation(
              phi=82 * DEGREES,
              theta=-80 * DEGREES,
          )
  
      def add_surface(self):
          self.surface = self.get_cos_exp_surface()
          self.add(self.surface)
  
      def add_graph(self):
          self.graph = self.get_graph()
          self.add(self.graph)
  
      def show_all_harmonic_surfaces(self):
          omega_tracker = self.omega_tracker
          formula = self.get_formula(str(1))
          L = self.axes.x_max
  
          self.begin_ambient_camera_rotation(rate=0.01)
          self.add_fixed_in_frame_mobjects(formula)
          self.wait(2)
          for n in range(2, self.n_iterations):
              if n > 5:
                  n_str = "n"
              else:
                  n_str = str(n)
              new_formula = self.get_formula(n_str)
              self.play(
                  Transform(formula, new_formula),
                  ApplyMethod(
                      omega_tracker.set_value,
                      n * PI / L
                  ),
              )
              self.wait(3)
  
      #
      def get_formula(self, n_str):
          n_str = "{" + n_str + "}"
          result = TexMobject(
              "\\cos\\left(",
              n_str, "(", "\\pi / L", ")", "{x}"
              "\\right)"
              "e^{-\\alpha (", n_str, "\\pi / L", ")^2",
              "{t}}",
              tex_to_color_map={
                  "{x}": GREEN,
                  "{t}": YELLOW,
                  "\\pi / L": MAROON_B,
                  n_str: MAROON_B,
              }
          )
          result.to_edge(UP)
          return result
  
  
  class Thumbnail(ShowHarmonicSurfaces):
      CONFIG = {
          "default_surface_config": {
              "resolution": (40, 30),
              # "resolution": (10, 10),
          },
          "graph_config": {
              "stroke_width": 8,
          },
      }
  
      def construct(self):
          self.setup_axes()
          self.initialize_parameter_trackers()
          self.add_surface()
          self.add_graph()
          #
          self.omega_tracker.set_value(3 * PI / 10)
          self.set_camera_orientation(
              theta=-70 * DEGREES,
          )
  
          axes = self.axes
          for axis in [axes.y_axis, axes.z_axis]:
              axis.numbers.set_opacity(0)
              axis.remove(*axis.numbers)
          axes.x_axis.label.set_opacity(0)
          axes.z_axis.label.set_opacity(0)
  
          for n in range(2, 16, 2):
              new_graph = self.get_time_slice_graph(
                  axes, self.func, t=n,
                  **self.graph_config
              )
              new_graph.set_shade_in_3d(True)
              new_graph.set_stroke(
                  width=8 / np.sqrt(n),
                  # opacity=1 / n**(1 / 4),
              )
              self.add(new_graph)
  
          words = TextMobject(
              "Sine waves + Linearity + Fourier = Solution"
          )
          words.set_width(FRAME_WIDTH - 1)
          words.to_edge(DOWN)
          words.shift(2 * DOWN)
          self.add_fixed_in_frame_mobjects(words)
  
          self.camera.frame_center.shift(DOWN)
          self.update_mobjects(0)
          self.surface.set_stroke(width=0.1)
          self.surface.set_fill(opacity=0.2)
  
  
  if __name__ == "__main__":
      module_name = os.path.basename(__file__)
      command_A = "manim -p -s -c '#2B2B2B' --video_dir ~/Downloads/  "
      command_B = module_name + " " + "SimpleCosExpGraph"
      os.system(command_A + command_B)
  
  