Example #1
0
 def test_b23(self):
     known = 3 * (self._t)**2 * (1 - self._t)
     i, p = 2, 3
     calc = bp.bernstein_polynomial(i, p, self._nti)
     self.assertTrue(self.same(known, calc))
Example #2
0
 def test_b33(self):
     known = (self._t)**3
     i, p = 3, 3
     calc = bp.bernstein_polynomial(i, p, self._nti)
     self.assertTrue(self.same(known, calc))
Example #3
0
 def test_b12(self):
     known = 2 * self._t * (1 - self._t)
     i, p = 1, 2
     calc = bp.bernstein_polynomial(i, p, self._nti)
     self.assertTrue(self.same(known, calc))
Example #4
0
 def test_b22(self):
     known = (self._t)**2
     i, p = 2, 2
     calc = bp.bernstein_polynomial(i, p, self._nti)
     self.assertTrue(self.same(known, calc))
Example #5
0
 def test_b01_and_verbose(self):
     known = 1 - self._t
     i, p = 0, 1
     verbose = True
     calc = bp.bernstein_polynomial(i, p, self._nti, verbose=verbose)
     self.assertTrue(self.same(known, calc))
Example #6
0
 def test_b11(self):
     known = self._t
     i, p = 1, 1
     calc = bp.bernstein_polynomial(i, p, self._nti)
     self.assertTrue(self.same(known, calc))
Example #7
0
 def test_b00_input_out_of_range_and_verbose(self):
     i, p = 0, 0
     verbose = True
     calc = bp.bernstein_polynomial(i, p, self._nti, verbose=verbose)
     self.assertIsNone(calc)
Example #8
0
 def test_b44(self):
     known = (self._t)**4
     i, p = 4, 4
     calc = bp.bernstein_polynomial(i, p, self._nti)
     self.assertTrue(self.same(known, calc))
Example #9
0
 def test_b34(self):
     known = 4 * (self._t)**3 * (1 - self._t)
     i, p = 3, 4
     calc = bp.bernstein_polynomial(i, p, self._nti)
     self.assertTrue(self.same(known, calc))
Example #10
0
    def __init__(self, config: str, verbose: bool = True):

        # abbreviations:
        # cp: control point; collection of control points forms the control net
        # cn: control net, composed of control points
        # n_cp: number of control points (int) per net
        # n_nets: number of control nets (int)

        if not Path(config).is_file():
            sys.exit(f"Error: cannot find file {config}")

        STEM = Path(config).stem

        config_dir = Path(config).parent

        if verbose:
            class_name = type(self).__name__
            print(f"This is {class_name}:")
            print(f"  processing config file: {config}")
            print(f"  located at: {config_dir}")

        with open(config) as fin:
            kwargs = json.load(fin)

        # config parameters without defaults, user specification required
        config_schema = (
            "bezier-type",
            "data-path",
            "control-nets",
            "control-points",
        )

        # check .json input schema
        for kw in config_schema:
            key = kwargs.get(kw, None)
            if not key:
                sys.exit(
                    f'Error: keyword "{kw}" not found in config input file.')

        bezier_type = kwargs.get("bezier-type")
        bezier_types = ("curve", "surface", "solid")
        if bezier_type not in bezier_types:
            sys.exit(f'Error: bezier-type "{bezier_type}" not supported.')

        data_path = kwargs.get("data-path")
        if data_path == ".":
            data_path = config_dir  # .csv files are in same folder as .json file
        data_path_expanded = Path(data_path).expanduser()
        cp_file = kwargs.get("control-points")
        cn_file = kwargs.get("control-nets")

        # number of time interval divisions nti_divisions
        # gives rise to the number of time intervals nti as
        # 2**nti_divisions = nti
        # 1 -> 2**1 = 2 (default)
        # 2 -> 2**2 = 4
        # 3 -> 2**3 = 8
        # 4 -> 2**4 = 64
        # etc.
        nti_divisions = kwargs.get("nti_divisions", 1)
        if nti_divisions < 1:
            print("Error: number of time interval divisions (nti_divisions)")
            print("must be a positive integer, [1, 2, 3 ...].")
            sys.exit(f"Currint nti_divisions = {nti_divisions}")

        # control point data
        # do not set alpha, let scatter3D control this, which
        # automatically provides an implied depth of field
        #
        control_points_alpha = kwargs.get("control-alpha", 0.5)
        control_points_color = kwargs.get("control-points-color", "red")
        control_points_label = kwargs.get("control-points-label", False)
        control_points_label_color = kwargs.get("control-points-label-color",
                                                "black")
        # control_points_alpha = kwargs.get("control-points-alpha", 0.5)
        control_points_marker = kwargs.get("control-points-marker", "o")
        control_points_path = kwargs.get("control-points-path", False)
        control_points_shown = kwargs.get("control-points-shown", True)
        control_points_size = kwargs.get("control-points-size", 50)

        # draw a specific control net (or nets)
        control_nets_shown = kwargs.get("control-nets-shown", [0])
        control_nets_linestyle = kwargs.get("control-nets-linestyle", "dashed")
        control_nets_linewidth = kwargs.get("control-nets-linewidth", 1.0)

        # Bezier interpolation data
        # the alpha channel (transparency) for the Bezier curve,
        # surface, or volume when rendered
        # do not set alpha, let scatter3D control this, which
        # automatically provides an implied depth of field
        #
        # bezier_points_alpha = kwargs.get("bezier-alpha", 0.9)
        bezier_points_color = kwargs.get("bezier-points-color", "blue")
        bezier_points_shown = kwargs.get("bezier-points-shown", True)
        bezier_points_size = kwargs.get("bezier-points-size", 10)
        #
        bezier_lines_shown = kwargs.get("bezier-lines-shown", False)
        bezier_lines_color = kwargs.get("bezier-lines-color", "black")
        bezier_linewidth = kwargs.get("bezier-linewidth", 1.0)

        surface_triangulation = kwargs.get("surface-triangulation",
                                           False)  # surface
        surface_t0_uv_triangulation = kwargs.get("surface-t0-uv-triangulation",
                                                 False)  # volume
        surface_t1_uv_triangulation = kwargs.get("surface-t1-uv-triangulation",
                                                 False)  # volume
        surface_u0_vt_triangulation = kwargs.get("surface-u0-vt-triangulation",
                                                 False)  # volume
        surface_u1_vt_triangulation = kwargs.get("surface-u1-vt-triangulation",
                                                 False)  # volume
        surface_v0_tu_triangulation = kwargs.get("surface-v0-tu-triangulation",
                                                 False)  # volume
        surface_v1_tu_triangulation = kwargs.get("surface-v1-tu-triangulation",
                                                 False)  # volume
        triangulation_alpha = kwargs.get("triangulation-alpha",
                                         1.0)  # surface or volume

        xlabel = kwargs.get("xlabel", "x")
        ylabel = kwargs.get("ylabel", "y")
        zlabel = kwargs.get("zlabel", "z")

        xlim = kwargs.get("xlim", None)
        ylim = kwargs.get("ylim", None)
        zlim = kwargs.get("zlim", None)

        camera_elevation = kwargs.get("camera-elevation", None)
        camera_azimuth = kwargs.get("camera-azimuth", None)

        XTICKS = kwargs.get("xticks", None)
        YTICKS = kwargs.get("yticks", None)
        ZTICKS = kwargs.get("zticks", None)

        Z_AXIS_LABEL_INVERTED = kwargs.get("z-axis-label-inverted", True)
        INTERACTIVE = kwargs.get("interactive",
                                 False)  # True shows plot, False does not
        SERIALZE = kwargs.get("serialize", False)
        LATEX = kwargs.get("latex", False)

        if LATEX:
            rc("font", **{
                "family": "serif",
                "serif": ["Computer Modern Roman"]
            })
            rc("text", usetex=True)

        # check existence of path and files
        # if not Path(data_path_expanded).is_dir():
        if not data_path_expanded.is_dir():
            sys.exit(f"Error: cannot find path: {data_path}")

        # cp_path_file = data_path_expanded + cp_file
        cp_path_file = data_path_expanded.joinpath(cp_file)
        # if not Path(cp_path_file).is_file():
        if not cp_path_file.is_file():
            sys.exit(f"Error: cannot find control points file: {cp_path_file}")

        # cn_path_file = data_path_expanded + cn_file
        cn_path_file = data_path_expanded.joinpath(cn_file)
        # if not Path(cn_path_file).is_file():
        if not cn_path_file.is_file():
            sys.exit(f"Error: cannot find control net file: {cn_path_file}")

        # file io data type specification
        data_type_string = "float"
        data_type_int = "i8"
        data_type_delimiter = ","
        n_headers = 0  # no headers in the csv files

        # avoid "magic" numbers, assign (0, 1, 2) to variables
        idx, idy, idz = (0, 1, 2)  # column indices from .csv file

        with open(cp_path_file) as fin:
            data = np.genfromtxt(
                cp_path_file,
                dtype=data_type_string,
                delimiter=data_type_delimiter,
                skip_header=n_headers,
            )
            cp_x = data[:, idx]
            cp_y = data[:, idy]
            cp_z = data[:, idz]

        with open(cn_path_file) as fin:
            nets = np.genfromtxt(
                cn_path_file,
                dtype=data_type_int,
                delimiter=data_type_delimiter,
                skip_header=n_headers,
            )

            if len(nets.shape) == 1:
                # handle special case of single net
                # https://numpy.org/devdocs/user/absolute_beginners.html#how-to-convert-a-1d-array-into-a-2d-array-how-to-add-a-new-axis-to-an-array
                nets = np.expand_dims(nets, axis=0)
                # otherwise, we have two or more nets, dims are ok

            n_nets, n_cp = nets.shape  # number (control nets, control points)

            if verbose:
                print(f"Number of control nets: {n_nets}")
                print(f"Number of control points per net: {n_cp}")

        fig = plt.figure(figsize=plt.figaspect(1.0), dpi=100)
        # fig = plt.figure(figsize=(6.5, 3.25), dpi=DPI)
        ax = fig.gca(projection="3d")
        # ax = plt.axes(projection="3d")
        # ax.plot3D(cp_x, cp_y, cp_z)
        # ax.scatter3D(cp_x, cp_y, cp_z, edgecolor="blue",
        #              facecolor=(0, 0, 0, 0), s=10)

        # example for control_nets_shown
        # [0]: show the first control net
        # [0, 2]: show the first and third control nets
        for net in [nets[k] for k in control_nets_shown]:

            net_x = [cp_x[i] for i in net]
            net_y = [cp_y[i] for i in net]
            net_z = [cp_z[i] for i in net]

            if control_points_path:
                ax.plot3D(
                    net_x,
                    net_y,
                    net_z,
                    color=control_points_color,
                    linestyle=control_nets_linestyle,
                    linewidth=control_nets_linewidth,
                    alpha=control_points_alpha,
                )

            if control_points_label or control_points_shown:
                for i, cp_index in enumerate(net):
                    if control_points_shown:
                        ax.scatter3D(
                            net_x[i],
                            net_y[i],
                            net_z[i],
                            edgecolor=control_points_color,
                            facecolor="white",
                            alpha=control_points_alpha,
                            marker=control_points_marker,
                            s=control_points_size,
                        )
                    if control_points_label:
                        ax.text(
                            net_x[i],
                            net_y[i],
                            net_z[i],
                            str(cp_index),
                            color=control_points_label_color,
                        )
                    if verbose:
                        print(
                            f"control net node {i} is control point {cp_index}"
                        )

            if bezier_points_shown or bezier_lines_shown:
                x, y, z = (0.0, 0.0, 0.0)

                # assumes number of control points same along each axis
                # for either 2D (or 3D, to come); 2D uses square root
                # and 3D will use cube root
                # n_cp_per_axis = int(np.sqrt(len(net)))
                # 1D case:
                if bezier_type == "curve":
                    n_cp_per_axis = len(net)

                elif bezier_type == "surface":
                    n_cp_per_axis = int(np.sqrt(len(net)))

                elif bezier_type == "solid":
                    n_cp_per_axis = int(np.cbrt(len(net)))

                p_degree = n_cp_per_axis - 1  # polynomial degree
                # nti = 2  # segment [0, 1] into nti intervals
                # nti = 2**4  # segment [0, 1] into nti intervals

                # segment [0, 1] into nti intervals
                nti = 2**nti_divisions

                # curve, surface, or solid
                t = np.linspace(0, 1, num=nti + 1, endpoint=True)

                # surface or solid
                u = np.linspace(0, 1, num=nti + 1, endpoint=True)

                # solid, won't use b/c solid triangulation will be 8 surface triangulations
                # v = np.linspace(0, 1, num=nti + 1, endpoint=True)

                if bezier_type == "curve":

                    Point = net

                    for i in np.arange(n_cp_per_axis):
                        b_i = bp.bernstein_polynomial(i, p_degree, nti)

                        if verbose:
                            print(f"b{i} = {b_i}")

                        x += b_i * cp_x[Point[i]]
                        y += b_i * cp_y[Point[i]]
                        z += b_i * cp_z[Point[i]]

                if bezier_type == "surface":

                    Point = np.reshape(net, (n_cp_per_axis, n_cp_per_axis))

                    for i in np.arange(n_cp_per_axis):
                        for j in np.arange(n_cp_per_axis):
                            b_i = bp.bernstein_polynomial(i, p_degree, nti)
                            b_j = bp.bernstein_polynomial(j, p_degree, nti)
                            bij = np.outer(b_i, b_j)

                            if verbose:
                                print(f"bij = {bij}")

                            x += bij * cp_x[Point[i][j]]
                            y += bij * cp_y[Point[i][j]]
                            z += bij * cp_z[Point[i][j]]

                    # triangulate parameter space,
                    # determine the triangles, cf
                    # https://matplotlib.org/3.1.1/gallery/mplot3d/trisurf3d_2.html
                    if surface_triangulation:

                        # convention here is reverse of the (x, y) convention of
                        # mesh grid, see
                        # https://numpy.org/doc/stable/reference/generated/numpy.meshgrid.html
                        u, t = np.meshgrid(u, t)
                        u, t = u.flatten(), t.flatten()

                        tri = mtri.Triangulation(u, t)
                        ax.plot_trisurf(
                            x.flatten(),
                            y.flatten(),
                            z.flatten(),
                            triangles=tri.triangles,
                            alpha=triangulation_alpha,
                        )
                        if verbose:
                            print("Triangulation is complete.")

                if bezier_type == "solid":

                    Point = np.reshape(
                        net, (n_cp_per_axis, n_cp_per_axis, n_cp_per_axis))

                    for i in np.arange(n_cp_per_axis):
                        for j in np.arange(n_cp_per_axis):
                            for k in np.arange(n_cp_per_axis):
                                b_i = bp.bernstein_polynomial(i, p_degree, nti)
                                b_j = bp.bernstein_polynomial(j, p_degree, nti)
                                b_k = bp.bernstein_polynomial(k, p_degree, nti)
                                bjk = np.outer(b_j, b_k)
                                bijk = np.reshape(np.outer(
                                    b_i, bjk), (len(b_i), len(b_j), len(b_k)))

                                if verbose:
                                    print(f"bijk = {bijk}")

                                x += bijk * cp_x[Point[i][j][k]]
                                y += bijk * cp_y[Point[i][j][k]]
                                z += bijk * cp_z[Point[i][j][k]]

                    if (surface_t0_uv_triangulation
                            or surface_t1_uv_triangulation
                            or surface_u0_vt_triangulation
                            or surface_u1_vt_triangulation
                            or surface_v0_tu_triangulation
                            or surface_v1_tu_triangulation):

                        # convention here is reverse of the (x, y) convention of
                        # mesh grid, see
                        # https://numpy.org/doc/stable/reference/generated/numpy.meshgrid.html
                        u, t = np.meshgrid(u, t)
                        u, t = u.flatten(), t.flatten()

                        tri = mtri.Triangulation(u, t)

                        if surface_t0_uv_triangulation:
                            ax.plot_trisurf(
                                x[0].flatten(),
                                y[0].flatten(),
                                z[0].flatten(),
                                triangles=tri.triangles,
                                alpha=triangulation_alpha,
                            )

                        if surface_t1_uv_triangulation:
                            ax.plot_trisurf(
                                x[-1].flatten(),
                                y[-1].flatten(),
                                z[-1].flatten(),
                                triangles=tri.triangles,
                                alpha=triangulation_alpha,
                            )

                        if surface_u0_vt_triangulation:
                            ax.plot_trisurf(
                                x[:, 0].flatten(),
                                y[:, 0].flatten(),
                                z[:, 0].flatten(),
                                triangles=tri.triangles,
                                alpha=triangulation_alpha,
                            )

                        if surface_u1_vt_triangulation:
                            ax.plot_trisurf(
                                x[:, -1].flatten(),
                                y[:, -1].flatten(),
                                z[:, -1].flatten(),
                                triangles=tri.triangles,
                                alpha=triangulation_alpha,
                            )

                        if surface_v0_tu_triangulation:
                            ax.plot_trisurf(
                                x[:, :, 0].flatten(),
                                y[:, :, 0].flatten(),
                                z[:, :, 0].flatten(),
                                triangles=tri.triangles,
                                alpha=triangulation_alpha,
                            )

                        if surface_v1_tu_triangulation:
                            ax.plot_trisurf(
                                x[:, :, -1].flatten(),
                                y[:, :, -1].flatten(),
                                z[:, :, -1].flatten(),
                                triangles=tri.triangles,
                                alpha=triangulation_alpha,
                            )

                        if verbose:
                            print("Triangulation is complete.")

                if bezier_points_shown:
                    ax.scatter3D(
                        x.flatten(),
                        y.flatten(),
                        z.flatten(),
                        color=bezier_points_color,
                        s=bezier_points_size,
                    )

                if bezier_lines_shown:
                    ax.plot(
                        x.flatten(),
                        y.flatten(),
                        z.flatten(),
                        color=bezier_lines_color,
                        linewidth=bezier_linewidth,
                    )

        ax.set_xlabel(xlabel)
        ax.set_ylabel(ylabel)
        ax.set_zlabel(zlabel)

        if Z_AXIS_LABEL_INVERTED:
            ax.zaxis.set_rotate_label(False)
            ax.zaxis.label.set_rotation(90)

        if XTICKS:
            ax.set_xticks(XTICKS)

        if YTICKS:
            ax.set_yticks(YTICKS)

        if ZTICKS:
            ax.set_zticks(ZTICKS)

        # fix coming in matplotlib 3.3.1 (current stable version is 3.2.2)
        # https://github.com/matplotlib/matplotlib/pull/17515
        # ax.set_box_aspect([1, 1, 1])
        # ax.set_proj_type('ortho') # optional - default is perspective (shown in image above)
        # set_axes_equal(ax) # IMPORTANT - this is also required
        # fig = plt.gcf()
        # fig.set_size_inches(5, 5)
        # ax.set_box_aspect(1)

        if xlim:
            ax.set_xlim(xlim)

        if ylim:
            ax.set_ylim(ylim)

        if zlim:
            ax.set_zlim(zlim)

        ax.view_init(elev=camera_elevation, azim=camera_azimuth)

        # plt.ioff()  # interactivity off
        if INTERACTIVE:
            plt.show()
        else:
            plt.show(block=False)

        if SERIALZE:
            extension = ".pdf"  # or ".svg"
            filename = STEM + extension
            fig.savefig(filename, bbox_inches="tight", pad_inches=0)
            print(f"Serialized file to {filename}")
Example #11
0
    def __init__(self, config):

        self.INITIALIZED = False

        # get client configuration, or if not specified, set to default configuration
        DEGREE = config.get("degree", 1)
        NTI_BISECTIONS = config.get("number-time-interval-bisections", 1)
        DISPLAY = config.get("display", True)
        AZIMUTH = config.get("camera-azimuth", 15)  # degrees
        ELEVATION = config.get("camera-elevation", 15)  # degrees
        DPI = config.get("dots-per-inch", 100)
        LATEX = config.get("latex", True)
        SERIALIZE = config.get("serialize", True)
        VERBOSE = config.get("verbose", True)
        Z_AXIS_LABEL_INVERTED = config.get("z-axis-label-inverted", True)

        if LATEX:
            rc("font", **{
                "family": "serif",
                "serif": ["Computer Modern Roman"]
            })
            rc("text", usetex=True)

        # DEGREE = 1  # e.g., p=1 linear, p=2 quadratic, p=3 cubic
        cp_t = np.arange(DEGREE + 1)  # control points in the t direction
        cp_u = np.arange(DEGREE + 1)  # control points in the u direction

        # control points in (t, u) space
        cp_tu = tuple((i, j) for i in cp_t for j in cp_u)
        cp_ts = [z[0] / DEGREE for z in cp_tu]
        cp_us = [z[1] / DEGREE for z in cp_tu]
        cp_bs = [0
                 for z in cp_tu]  # plot control points at zero for the B axis

        # number of time intervals
        # e.g., 4 time intervals implies 5 evaluation points along an axis, t or u
        # nti = 2 ** 1  # for minimal interpolation
        # nti = 2 ** 1  # for LaTeX figures
        nti = 2**NTI_BISECTIONS
        # azimuth, elevation = (-15, 15)  # degrees
        # azimuth, elevation = (15, 15)  # degrees
        # azimuth, elevation = (-75, 15)  # degrees

        # bases = np.array([np.array([])])

        if VERBOSE:
            print(f"Computing Bezier surface with degree p={DEGREE}")
            print(f"with number of time intervals nti={nti}")

        for i in cp_t:
            for j in cp_u:

                b_i = bp.bernstein_polynomial(i, DEGREE, nti)
                b_j = bp.bernstein_polynomial(j, DEGREE, nti)
                bij = np.outer(b_i, b_j)
                if VERBOSE:
                    print(f"i={i}, j={j}")
                    print(f"bij = {bij}")

                # fig = plt.figure()
                fig = plt.figure(figsize=plt.figaspect(1.0), dpi=DPI)
                # fig = plt.figure(figsize=(6.5, 3.25), dpi=DPI)
                ax = fig.gca(projection="3d")
                # ax.view_init(elevation, azimuth)
                ax.view_init(ELEVATION, AZIMUTH)
                plt.subplots_adjust(left=0,
                                    bottom=0,
                                    right=1,
                                    top=1,
                                    wspace=0,
                                    hspace=0)

                t = np.linspace(0, 1, nti + 1)
                # for projecting onto [t, B] and [u, B] planes
                zeros = [0 for i in range(len(t))]

                # on the (t, u) grid:
                tij = np.outer(
                    t, np.ones(nti + 1))  # all of the t values from [0, 1]
                uij = np.outer(np.ones(nti + 1),
                               t)  # all of the u values from [0, 1]
                if VERBOSE:
                    print(f"tij = {tij}")
                    print(f"uij = {uij}")

                # plot the control net in the [t, u] plane
                ax.scatter3D(cp_ts,
                             cp_us,
                             cp_bs,
                             edgecolor="black",
                             facecolor="black",
                             s=20)

                # plot the Bezier basis functions in the [u, B] plane
                for II in cp_t:
                    ax.plot3D(
                        t,
                        zeros,
                        bp.bernstein_polynomial(II, DEGREE, nti),
                        color="gray",
                        linewidth=0.5,
                    )

                # and highlight the current basis function
                ax.plot3D(
                    t,
                    zeros,
                    bp.bernstein_polynomial(i, DEGREE, nti),
                    color="red",
                    linewidth=1.5,
                )

                # plot the Bezier basis functions in the [t, B] plane
                for JJ in cp_u:
                    ax.plot3D(
                        zeros,
                        t,
                        bp.bernstein_polynomial(JJ, DEGREE, nti),
                        color="gray",
                        linewidth=0.5,
                    )

                # and highlight the current basis function
                ax.plot3D(
                    zeros,
                    t,
                    bp.bernstein_polynomial(j, DEGREE, nti),
                    color="green",
                    linewidth=1.5,
                )

                # surf = ax.plot_surface(tij, uij, bij, alpha=0.8)
                ax.plot_surface(tij, uij, bij, alpha=0.8)

                ax.set_xlabel(r"$t$")
                ax.set_ylabel(r"$u$")
                ax.set_zlabel(r"$B^{" + str(DEGREE) + "}_{" + str(i) + ", " +
                              str(j) + "}(t, u)$")
                if Z_AXIS_LABEL_INVERTED:
                    ax.zaxis.set_rotate_label(False)
                    ax.zaxis.label.set_rotation(90)

                ax.set_xlim([0, 1])
                ax.set_ylim([0, 1])
                ax.set_zlim([0, 1])
                ax.xaxis.set_major_locator(MultipleLocator(0.25))
                ax.yaxis.set_major_locator(MultipleLocator(0.25))
                ax.zaxis.set_major_locator(MultipleLocator(0.25))

                if DISPLAY:
                    # plt.show()
                    plt.show(block=False)

                if SERIALIZE:
                    extension = ".pdf"  # or '.svg'
                    bstring = "B(p=" + str(DEGREE) + ")_" + str(i) + "_"
                    bstring += str(j) + extension
                    # fig.savefig(bstring, bbox_inches="tight")
                    fig.savefig(bstring, bbox_inches="tight", pad_inches=0)
                    print(f"Serialized file as {bstring}")

        self.INITIALIZED = True