github.com/network-quality/goresponsiveness@v0.0.0-20240129151524-343954285090/tools/graphing.py (about)

     1  import re
     2  
     3  import pandas as pd
     4  import matplotlib.pyplot as plt
     5  import matplotlib.backends.backend_pdf # For pdf output
     6  import os
     7  import argparse
     8  import pprint
     9  
    10  parser = argparse.ArgumentParser(description='Make some graphs using CSV files!')
    11  # parser.add_argument("filename", type=argparse.FileType('r'))
    12  parser.add_argument("filename", type=str, help="Put a single one of the log files from a set here, " +
    13                                                 "and it will parse the rest")
    14  
    15  
    16  # Regex should be described below in match_filename(filename, component)
    17  __COMPONENTS__ = {
    18      "foreign": "-foreign-",
    19      "self": "-self-",
    20      "download": "-throughput-download-",
    21      "upload": "-throughput-upload-",
    22      "granular": "-throughput-granular-",
    23  }
    24  
    25  __LINECOLOR__ = {
    26      "download": "#0095ed",
    27      "upload": "#44BB66",
    28      "foreign": "#ac7ae7", # "#7522d7",
    29      "selfUp": "#7ccf93",
    30      "selfDown": "#4cb4f2" # "#7fcaf6",
    31  }
    32  
    33  
    34  def match_filename(filename, component):
    35      """
    36      Input a filename and a component regex component to match the filename to its <start><component><end> regex.
    37      Returns a match object with groups: start, component, end.
    38  
    39      :param filename: String of filename
    40      :param component: String to add into the regex
    41      :return: Match object or None
    42      """
    43      regex = f"(?P<start>.*)(?P<component>{component})(?P<end>.*)"
    44      return re.match(regex, filename)
    45  
    46  
    47  def seconds_since_start(dfs, start, column_name="SecondsSinceStart"):
    48      """
    49      Adds "Seconds Since Start" column to all DataFrames in List of DataFrames,
    50      based on "CreationTime" column within them and start time passed.
    51  
    52      :param dfs: List of DataFrames. Each DataFrame MUST contain DateTime column named "CreationTime"
    53      :param start: DateTime start time
    54      :param column_name: String of column name to add, default "SecondsSinceStart"
    55      :return: Inplace addition of column using passed column name
    56      """
    57      for df in dfs:
    58          df[column_name] = (df["CreationTime"] - start).apply(pd.Timedelta.total_seconds)
    59  
    60  
    61  def find_earliest(dfs):
    62      """
    63      Returns earliest DateTime in List of DataFrames based on "CreationTime" column within them.
    64      ASSUMES DATAFRAMES ARE SORTED
    65  
    66      :param dfs: List of DataFrames. Each DataFrame MUST contain DateTime column named "CreationTime" and MUST BE SORTED by it.
    67      :return: DateTime of earliest time within all dfs.
    68      """
    69      earliest = dfs[0]["CreationTime"].iloc[0]
    70      for df in dfs:
    71          print(f"A data frame: {df['CreationTime']}")
    72          if df["CreationTime"].iloc[0] < earliest:
    73              earliest = df["CreationTime"].iloc[0]
    74      return earliest
    75  
    76  
    77  def time_since_start(dfs, start, column_name="TimeSinceStart"):
    78      """
    79      Adds "Seconds Since Start" column to all DataFrames in List of DataFrames,
    80      based on "CreationTime" column within them and start time passed.
    81  
    82      :param dfs: List of DataFrames. Each DataFrame MUST contain DateTime column named "CreationTime"
    83      :param start: DateTime start time
    84      :param column_name: String of column name to add, default "SecondsSinceStart"
    85      :return: Inplace addition of column using passed column name
    86      """
    87      for df in dfs:
    88          df[column_name] = df["CreationTime"] - start
    89  
    90  
    91  def probeClean(df):
    92      # ConnRTT and ConnCongestionWindow refer to Underlying Connection
    93      df.columns = ["CreationTime", "NumRTT", "Duration", "ConnRTT", "ConnCongestionWindow", "Type", "Algorithm", "Empty"]
    94      df = df.drop(columns=["Empty"])
    95      df["CreationTime"] = pd.to_datetime(df["CreationTime"], format="%m-%d-%Y-%H-%M-%S.%f")
    96      df["Type"] = df["Type"].apply(str.strip)
    97      df["ADJ_Duration"] = df["Duration"] / df["NumRTT"]
    98      df = df.sort_values(by=["CreationTime"])
    99      return df
   100  
   101  
   102  def throughputClean(df):
   103      df.columns = ["CreationTime", "Throughput", "NumberActiveConnections", "NumberConnections", "Empty"]
   104      df = df.drop(columns=["Empty"])
   105      df["CreationTime"] = pd.to_datetime(df["CreationTime"], format="%m-%d-%Y-%H-%M-%S.%f")
   106      df["ADJ_Throughput"] = df["Throughput"] / 1000000
   107      df = df.sort_values(by=["CreationTime"])
   108      return df
   109  
   110  
   111  def granularClean(df):
   112      df.columns = ["CreationTime", "Throughput", "ID", "RTT", "Cwnd", "Type", "Empty"]
   113      df = df.drop(columns=["Empty"])
   114      df["CreationTime"] = pd.to_datetime(df["CreationTime"], format="%m-%d-%Y-%H-%M-%S.%f")
   115      df["Type"] = df["Type"].apply(str.strip)
   116      df["ADJ_Throughput"] = df["Throughput"] / 1000000
   117      df = df.sort_values(by=["CreationTime"])
   118      return df
   119  
   120  
   121  def make90Percentile(df):
   122      df = df.sort_values(by=["ADJ_Duration"])
   123      df = df.reset_index()
   124      df = df.iloc[:int(len(df)*.9)]
   125      df = df.sort_values(by=["CreationTime"])
   126      return df
   127  
   128  
   129  def main(title, paths):
   130      # Data Ingestion
   131      foreign = pd.read_csv(paths["foreign"])
   132      self = pd.read_csv(paths["self"])
   133      download = pd.read_csv(paths["download"])
   134      upload = pd.read_csv(paths["upload"])
   135      granular = pd.read_csv(paths["granular"])
   136  
   137      # Data Cleaning
   138      foreign = probeClean(foreign)
   139      self = probeClean(self)
   140      download = throughputClean(download)
   141      upload = throughputClean(upload)
   142      granular = granularClean(granular)
   143  
   144      # Data Separation
   145      selfUp = self[self["Type"] == "SelfUp"]
   146      selfUp = selfUp.reset_index()
   147      selfDown = self[self["Type"] == "SelfDown"]
   148      selfDown = selfDown.reset_index()
   149      granularUp = granular[granular["Type"] == "Upload"]
   150      granularUp = granularUp.reset_index()
   151      granularDown = granular[granular["Type"] == "Download"]
   152      granularDown = granularDown.reset_index()
   153  
   154  
   155  
   156      # Moving Average
   157      foreign["DurationMA10"] = foreign["ADJ_Duration"].rolling(window=10).mean()
   158      selfUp["DurationMA10"] = selfUp["ADJ_Duration"].rolling(window=10).mean()
   159      selfDown["DurationMA10"] = selfDown["ADJ_Duration"].rolling(window=10).mean()
   160  
   161      # Normalize
   162      dfs = [foreign, selfUp, selfDown, download, upload, granularUp, granularDown]
   163      time_since_start(dfs, find_earliest(dfs))
   164      seconds_since_start(dfs, find_earliest(dfs))
   165  
   166      yCol = "SecondsSinceStart"
   167  
   168      # stacked_bar_throughput(upload, granularUp, "SecondsSinceStart", "ADJ_Throughput", title + " Upload Stacked",
   169      #                       "Upload Throughput MB/s")
   170      # stacked_bar_throughput(download, granularDown, "SecondsSinceStart", "ADJ_Throughput", title + " Download Stacked",
   171      #                       "Download Throughput MB/s")
   172      dfs_dict = {
   173          "foreign": foreign,
   174          "self": self,
   175          "download": download,
   176          "upload": upload,
   177          "granular": granular,
   178          "selfUp": selfUp,
   179          "selfDown": selfDown,
   180          "granularUp": granularUp,
   181          "granularDown": granularDown
   182      }
   183      fig, ax = plt.subplots()
   184      fig.canvas.manager.set_window_title(title + " Standard")
   185      graph_normal(dfs_dict, "SecondsSinceStart", ax, title + " Standard")
   186  
   187      fig, ax = plt.subplots()
   188      fig.canvas.manager.set_window_title(title + " Standard ms")
   189      graph_normal_ms(dfs_dict, "SecondsSinceStart", ax, title + " Standard ms")
   190      
   191      # Both Upload/Download Granular on one figure
   192      fig, axs = plt.subplots(2, 1)
   193      fig.canvas.manager.set_window_title(title + " Combined Throughput")
   194      stacked_area_throughput(download, granularDown, "SecondsSinceStart", "ADJ_Throughput", axs[0],
   195                              title + " Download Stacked",
   196                              "Download Throughput MB/s", __LINECOLOR__["download"])
   197      stacked_area_throughput(upload, granularUp, "SecondsSinceStart", "ADJ_Throughput", axs[1],
   198                              title + " Upload Stacked",
   199                              "Upload Throughput MB/s",  __LINECOLOR__["upload"])
   200      # Individual figure
   201      fig, ax = plt.subplots()
   202      fig.canvas.manager.set_window_title(title + " Download Throughput")
   203      stacked_area_throughput(download, granularDown, "SecondsSinceStart", "ADJ_Throughput", ax,
   204                              title + " Download Stacked",
   205                              "Download Throughput MB/s",  __LINECOLOR__["download"])
   206      fig, ax = plt.subplots()
   207      fig.canvas.manager.set_window_title(title + " Upload Throughput")
   208      stacked_area_throughput(upload, granularUp, "SecondsSinceStart", "ADJ_Throughput", ax,
   209                              title + " Upload Stacked",
   210                              "Upload Throughput MB/s",  __LINECOLOR__["upload"])
   211  
   212      def Percent90():
   213          ######### Graphing Removing 90th Percentile
   214          nonlocal selfUp
   215          nonlocal selfDown
   216          nonlocal foreign
   217          selfUp = make90Percentile(selfUp)
   218          selfDown = make90Percentile(selfDown)
   219          foreign = make90Percentile(foreign)
   220  
   221          # Recalculate MA
   222          foreign["DurationMA5"] = foreign["ADJ_Duration"].rolling(window=5).mean()
   223          selfUp["DurationMA5"] = selfUp["ADJ_Duration"].rolling(window=5).mean()
   224          selfDown["DurationMA5"] = selfDown["ADJ_Duration"].rolling(window=5).mean()
   225  
   226          # Graphing Complete
   227          fig, ax = plt.subplots()
   228          ax.set_title(title + " 90th Percentile (ordered lowest to highest duration)")
   229          # ax.plot(foreign[yCol], foreign["ADJ_Duration"], "b.", label="foreign")
   230          # ax.plot(selfUp[yCol], selfUp["ADJ_Duration"], "r.", label="selfUP")
   231          # ax.plot(selfDown[yCol], selfDown["ADJ_Duration"], "c.", label="selfDOWN")
   232          ax.plot(foreign[yCol], foreign["DurationMA5"], "b--", label="foreignMA")
   233          ax.plot(selfUp[yCol], selfUp["DurationMA5"], "r--", label="selfUPMA")
   234          ax.plot(selfDown[yCol], selfDown["DurationMA5"], "c--", label="selfDOWNMA")
   235          ax.set_ylim([0, max(foreign["ADJ_Duration"].max(), selfUp["ADJ_Duration"].max(), selfDown["ADJ_Duration"].max())])
   236          ax.legend(loc="upper left")
   237  
   238          secax = ax.twinx()
   239          secax.plot(download[yCol], download["ADJ_Throughput"], "g-", label="download (MB/s)")
   240          secax.plot(granularDown[granularDown["ID"] == 0][yCol], granularDown[granularDown["ID"] == 0]["ADJ_Throughput"],
   241                     "g--", label="Download Connection 0 (MB/S)")
   242          secax.plot(upload[yCol], upload["ADJ_Throughput"], "y-", label="upload (MB/s)")
   243          secax.plot(granularUp[granularUp["ID"] == 0][yCol], granularUp[granularUp["ID"] == 0]["ADJ_Throughput"], "y--",
   244                     label="Upload Connection 0 (MB/S)")
   245          secax.legend(loc="upper right")
   246  
   247      # Percent90()
   248  
   249  
   250  def graph_normal_ms(dfs, xcolumn, ax, title):
   251      ax.set_title(title)
   252      ax.set_xlabel("Seconds Since Start (s)")
   253  
   254      # To plot points
   255      # ax.plot(dfs["foreign"][xcolumn], dfs["foreign"]["ADJ_Duration"], "b.", label="foreign")
   256      # ax.plot(dfs["selfUp"][xcolumn], dfs["selfUp"]["ADJ_Duration"], "r.", label="selfUP")
   257      # ax.plot(dfs["selfDown"][xcolumn], dfs["selfDown"]["ADJ_Duration"], "c.", label="selfDOWN")
   258      dfs["foreign"]["DurationMA10ms"] = dfs["foreign"]["ADJ_Duration"].rolling(window=10, step=10).mean() * 1000
   259      dfs["selfUp"]["DurationMA10ms"] = dfs["selfUp"]["ADJ_Duration"].rolling(window=10, step=10).mean() * 1000
   260      dfs["selfDown"]["DurationMA10ms"] = dfs["selfDown"]["ADJ_Duration"].rolling(window=10, step=10).mean() * 1000
   261      # Plot lines
   262      ax.plot(dfs["foreign"][xcolumn][dfs["foreign"]["DurationMA10ms"].notnull()], dfs["foreign"]["DurationMA10ms"][dfs["foreign"]["DurationMA10ms"].notnull()], "--", linewidth=2, color=__LINECOLOR__["foreign"], label="foreignMA10 (ms)")
   263      ax.plot(dfs["selfUp"][xcolumn][dfs["selfUp"]["DurationMA10ms"].notnull()], dfs["selfUp"]["DurationMA10ms"][dfs["selfUp"]["DurationMA10ms"].notnull()], "--", linewidth=2, color=__LINECOLOR__["selfUp"], label="selfUpMA10 (ms)")
   264      ax.plot(dfs["selfDown"][xcolumn][dfs["selfDown"]["DurationMA10ms"].notnull()], dfs["selfDown"]["DurationMA10ms"][dfs["selfDown"]["DurationMA10ms"].notnull()], "--", linewidth=2, color=__LINECOLOR__["selfDown"], label="selfDownMA10 (ms)")
   265      ax.set_ylim([0, max(dfs["foreign"]["DurationMA10ms"].max(), dfs["selfUp"]["DurationMA10ms"].max(), dfs["selfDown"]["DurationMA10ms"].max()) * 1.01])
   266      ax.set_ylabel("RTT (ms)")
   267      ax.legend(loc="upper left", title="Probes")
   268  
   269  
   270      secax = ax.twinx()
   271      secax.plot(dfs["download"][xcolumn], dfs["download"]["ADJ_Throughput"], "-", linewidth=2, color=__LINECOLOR__["download"], label="download (MB/s)")
   272      # secax.plot(dfs.granularDown[dfs.granularDown["ID"] == 0][xcolumn], dfs.granularDown[dfs.granularDown["ID"] == 0]["ADJ_Throughput"],
   273      #            "g--", label="Download Connection 0 (MB/S)")
   274      secax.plot(dfs["upload"][xcolumn], dfs["upload"]["ADJ_Throughput"], "-", linewidth=2, color=__LINECOLOR__["upload"], label="upload (MB/s)")
   275      # secax.plot(dfs.granularUp[dfs.granularUp["ID"] == 0][xcolumn], dfs.granularUp[dfs.granularUp["ID"] == 0]["ADJ_Throughput"], "y--",
   276      #            label="Upload Connection 0 (MB/S)")
   277      secax.set_ylabel("Throughput (MB/s)")
   278      secax.legend(loc="upper right")
   279  
   280  
   281  def graph_normal(dfs, xcolumn, ax, title):
   282      ax.set_title(title)
   283      ax.set_xlabel("Seconds Since Start (s)")
   284  
   285      # To plot points
   286      # ax.plot(dfs["foreign"][xcolumn], dfs["foreign"]["ADJ_Duration"], "b.", label="foreign")
   287      # ax.plot(dfs["selfUp"][xcolumn], dfs["selfUp"]["ADJ_Duration"], "r.", label="selfUP")
   288      # ax.plot(dfs["selfDown"][xcolumn], dfs["selfDown"]["ADJ_Duration"], "c.", label="selfDOWN")
   289      # Plot lines
   290      ax.plot(dfs["foreign"][xcolumn], dfs["foreign"]["DurationMA10"], "--", linewidth=2, color=__LINECOLOR__["foreign"], label="foreignMA10 (s)")
   291      ax.plot(dfs["selfUp"][xcolumn], dfs["selfUp"]["DurationMA10"], "--", linewidth=2, color=__LINECOLOR__["selfUp"], label="selfUpMA10 (s)")
   292      ax.plot(dfs["selfDown"][xcolumn], dfs["selfDown"]["DurationMA10"], "--", linewidth=2, color=__LINECOLOR__["selfDown"], label="selfDownMA10 (s)")
   293      ax.set_ylim([0, max(dfs["foreign"]["DurationMA10"].max(), dfs["selfUp"]["DurationMA10"].max(), dfs["selfDown"]["DurationMA10"].max()) * 1.01])
   294      ax.set_ylabel("RTT (s)")
   295      ax.legend(loc="upper left", title="Probes")
   296  
   297  
   298      secax = ax.twinx()
   299      secax.plot(dfs["download"][xcolumn], dfs["download"]["ADJ_Throughput"], "-", linewidth=2, color=__LINECOLOR__["download"], label="download (MB/s)")
   300      # secax.plot(dfs.granularDown[dfs.granularDown["ID"] == 0][xcolumn], dfs.granularDown[dfs.granularDown["ID"] == 0]["ADJ_Throughput"],
   301      #            "g--", label="Download Connection 0 (MB/S)")
   302      secax.plot(dfs["upload"][xcolumn], dfs["upload"]["ADJ_Throughput"], "-", linewidth=2, color=__LINECOLOR__["upload"], label="upload (MB/s)")
   303      # secax.plot(dfs.granularUp[dfs.granularUp["ID"] == 0][xcolumn], dfs.granularUp[dfs.granularUp["ID"] == 0]["ADJ_Throughput"], "y--",
   304      #            label="Upload Connection 0 (MB/S)")
   305      secax.set_ylabel("Throughput (MB/s)")
   306      secax.legend(loc="upper right")
   307      
   308  
   309  def stacked_area_throughput(throughput_df, granular, xcolumn, ycolumn, ax, title, label, linecolor="black"):
   310  
   311      print(f"Stacked area throughput!")
   312      ax.set_title(title)
   313  
   314      ax.yaxis.tick_right()
   315      ax.yaxis.set_label_position("right")
   316      ax.set_xlabel("Seconds Since Start (s)")
   317      ax.set_ylabel("Throughput (MB/s)")
   318      # ax.set_xticks(range(0, round(granular[xcolumn].max()) + 1)) # Ticks every 1 second
   319  
   320      # Plot Main Throughput
   321      ax.plot(throughput_df[xcolumn], throughput_df[ycolumn], "-", color="white", linewidth=3)
   322      ax.plot(throughput_df[xcolumn], throughput_df[ycolumn], "-", color=linecolor, linewidth=2, label=label)
   323  
   324      df_gran = granular.copy()
   325  
   326      # df_gran["bucket"] = df_gran[xcolumn].round(0) # With rounding
   327      df_gran["bucket"] = df_gran[xcolumn]  # Without rounding (csv creation time points need to be aligned)
   328      df_gran = df_gran.set_index(xcolumn)
   329  
   330      buckets = pd.DataFrame(df_gran["bucket"].unique())
   331      buckets.columns = ["bucket"]
   332      buckets = buckets.set_index("bucket")
   333      for id in sorted(df_gran["ID"].unique()):
   334          buckets[id] = df_gran[ycolumn][df_gran["ID"] == id]
   335      buckets = buckets.fillna(0)
   336  
   337      # Plot Stacked Area Throughput
   338      ax.stackplot(buckets.index, buckets.transpose())
   339      ax.legend(loc="upper right")
   340  
   341  
   342  def stacked_bar_throughput(throughput_df, granular, xcolumn, ycolumn, ax, title, label, linecolor="black"):
   343      ax.set_title(title)
   344  
   345      ax.yaxis.tick_right()
   346      ax.yaxis.set_label_position("right")
   347      ax.set_xlabel("Seconds Since Start (s)")
   348      ax.set_ylabel("Throughput (MB/s)")
   349      # ax.set_xticks(range(0, round(granular[xcolumn].max()) + 1)) # Ticks every 1 second
   350  
   351      # Plot Main Throughput
   352      ax.plot(throughput_df[xcolumn], throughput_df[ycolumn], "-", color=linecolor, label=label)
   353  
   354      df_gran = granular.copy()
   355  
   356      # df_gran["bucket"] = df_gran[xcolumn].round(0) # With rounding
   357      df_gran["bucket"] = df_gran[xcolumn] # Without rounding (csv creation time points need to be aligned)
   358  
   359      buckets = pd.DataFrame(df_gran["bucket"].unique())
   360      buckets.columns = ["bucket"]
   361      buckets = buckets.set_index("bucket")
   362      buckets[xcolumn] = df_gran.drop_duplicates(subset=["bucket"]).reset_index()[xcolumn]
   363      buckets["bottom"] = 0
   364      for id in sorted(df_gran["ID"].unique()):
   365          ax.bar(df_gran[xcolumn][df_gran["ID"] == id],
   366                 df_gran[ycolumn][df_gran["ID"] == id],
   367                 width=.1, bottom=buckets.iloc[len(buckets) - len(df_gran[df_gran["ID"] == id]):]["bottom"]
   368                 )
   369          # ,label=f"Download Connection {id}")
   370          buckets["toadd_bottom"] = (df_gran[df_gran["ID"] == id]).set_index("bucket")[ycolumn]
   371          buckets["toadd_bottom"] = buckets["toadd_bottom"].fillna(0)
   372          buckets["bottom"] += buckets["toadd_bottom"]
   373  
   374      ax.legend(loc="upper right")
   375  
   376  
   377  def find_files(directory):
   378      matches = {}
   379  
   380      files = os.listdir(directory)
   381      for file in files:
   382          if os.path.isfile(directory + file):
   383              for name in __COMPONENTS__:
   384                  match = match_filename(file, __COMPONENTS__[name])
   385                  if match is not None:
   386                      start = match.group("start")
   387                      end = match.group("end")
   388                      if start not in matches:
   389                          matches[start] = {}
   390                      if end not in matches[start]:
   391                          matches[start][end] = {}
   392                      if name in matches[start][end]:
   393                          print("ERROR ALREADY FOUND A FILE THAT HAS THE SAME MATCHING")
   394                      matches[start][end][name] = directory + file
   395      return matches
   396  
   397  
   398  def find_matching_files(directory, filename):
   399      matches = {}
   400  
   401      # First determine the file's structure
   402      match = match_filename(os.path.basename(filename), "|".join(__COMPONENTS__.values()))
   403      if match is not None:
   404          file_start = match.group("start")
   405          file_end = match.group("end")
   406      else:
   407          print(f"ERROR COULD NOT MATCH FILE TO KNOWN SCHEMA: {filename}")
   408          return matches
   409  
   410      # Find its other matching files
   411      files = os.listdir(directory)
   412      for file in files:
   413          if os.path.isfile(directory + file):
   414              for name in __COMPONENTS__:
   415                  match = match_filename(file, __COMPONENTS__[name])
   416                  if match is not None:
   417                      start = match.group("start")
   418                      end = match.group("end")
   419                      if file_start == start and file_end == end:
   420                          if start not in matches:
   421                              matches[start] = {}
   422                          if end not in matches[start]:
   423                              matches[start][end] = {}
   424                          if name in matches[start][end]:
   425                              print("ERROR ALREADY FOUND A FILE THAT HAS THE SAME MATCHING")
   426                          matches[start][end][name] = directory + file
   427      return matches
   428  
   429  
   430  def generate_paths():
   431      return {
   432          "foreign": "",
   433          "self": "",
   434          "download": "",
   435          "upload": "",
   436          "granular": "",
   437      }
   438  
   439  
   440  def make_graphs(files, save):
   441      num_fig = 1
   442      for start in files:
   443          x = 0
   444          for end in files[start]:
   445              # Check if it contains all file fields
   446              containsALL = True
   447              for key in __COMPONENTS__:
   448                  if key not in files[start][end]:
   449                      containsALL = False
   450              # If we don't have all files then loop to next one
   451              if not containsALL:
   452                  continue
   453  
   454              print(f"About to call main()")
   455              main(start + " - " + str(x), files[start][end])
   456              if save:
   457                  pdf = matplotlib.backends.backend_pdf.PdfPages(f"{start} - {x}.pdf")
   458                  for fig in range(num_fig, plt.gcf().number + 1):
   459                      plt.figure(fig).set(size_inches=(11, 6.1875))  # 16:9 ratio for screens (11 x 6.1875) # 11 x 8.5 for page size
   460                      plt.figure(fig).tight_layout()
   461                      pdf.savefig(fig)
   462                      plt.figure(fig).set(size_inches=(10, 6.6))
   463                      plt.figure(fig).tight_layout()
   464                  pdf.close()
   465                  num_fig = plt.gcf().number + 1
   466              x += 1
   467  
   468  
   469  # Press the green button in the gutter to run the script.
   470  if __name__ == '__main__':
   471      ARGS = parser.parse_args()
   472      paths = generate_paths()
   473  
   474      print(f"Looking for files in directory: {os.path.dirname(ARGS.filename)}")
   475      # files = find_files(os.path.dirname(ARGS.filename) + "/")
   476      if os.path.isfile(ARGS.filename):
   477          files = find_matching_files(os.path.dirname(ARGS.filename) + "/", ARGS.filename)
   478      elif os.path.isdir(ARGS.filename):
   479          files = find_files(ARGS.filename)
   480      else:
   481          print("Error: filename passed is not recognized as a file or directory.")
   482          exit()
   483  
   484      print("Found files:")
   485      pprint.pprint(files, indent=1)
   486      make_graphs(files, True)
   487      plt.show()