code: #%%
  import numpy as np 
  import pandas as pd 
  import futileprot.viz
  import altair as alt
  import altair_saver
  import scipy.stats
  colors, palette = futileprot.viz.altair_style()
  
  # Add metadata
  DATE = '2021-08-14'
  RUN_NO = 1
  STRAINS = 'DoubleKO'
  MEDIUM = 'acetate'
  
  # Load the measurement data
  data = pd.read_csv(f'./output/{DATE}_r{RUN_NO}_{STRAINS}_{MEDIUM}_exponential_phase.csv')
  
  # Perform a simplistic inference of the growth rate to get a sense of what
  # the result is.
  # data = data.groupby(['strain', 'elapsed_time_hr']).mean().reset_index()
  data = data[['strain', 'elapsed_time_hr', 'od_600nm']]
  
  # For each strain, infer the growth rate and compute the fit
  layout = False
  for g, d in data.groupby(['strain']):
      time_range = np.linspace(0, 1.25 * d['elapsed_time_hr'].max(), 10)
  
      # Perform the regression
      popt = scipy.stats.linregress(d['elapsed_time_hr'], np.log(d['od_600nm']))
      slope, intercept, err = popt[0], popt[1], popt[-1]
      print(f'{g}, {MEDIUM}: µ = {slope:0.3f} ± {err:0.3f} per hr.')
      # Compute the fit
      fit = np.exp(intercept + slope * time_range)
      fit_df = pd.DataFrame([])
      fit_df['elapsed_time_hr'] = time_range
      fit_df['od_600nm'] = fit
  
      # Generate the plot
      points = alt.Chart(
                          data=d, 
                          width=300,  
                          height=150
                      ).mark_point(
                          color=colors['primary_blue']
                      ).encode(
                          x=alt.X('elapsed_time_hr:Q', title='elapsed time [hr]'),
                          y=alt.Y('od_600nm:Q', title='optical density [a.u]',
                                  scale=alt.Scale(type='log'))
                      )
  
      fit = alt.Chart(data=fit_df,
                      title=f'{g}, {MEDIUM}: µ = {slope:0.3f} ± {err:0.3f} per hr.'
                      ).mark_line(   
                          color=colors['primary_blue']
                      ).encode(
                          x='elapsed_time_hr:Q',
                          y='od_600nm:Q'
                      )
      merge = points + fit
      if layout == False:
          layout = merge
      else: 
          layout &= merge
  
  altair_saver.save(layout, f'output/{DATE}_r{RUN_NO}_{STRAINS}_{MEDIUM}_fits.png',
                  scale_factor=2)
  # %%
   #%%
  import numpy as np 
  import pandas as pd 
  import futileprot.io
  import futileprot.viz
  import altair as alt 
  import altair_saver
  colors, palette = futileprot.viz.altair_style()
  
  # Define experiment parameters
  DATE = '2021-08-12'
  STRAINS = 'DoubleKO'
  MEDIUM = 'acetate'
  RUN_NO = 1
  ROOT = '../../../..'
  SKIPROWS = 36 
  OD_BOUNDS = [0.03, 0.18]
  
  # Add the well identifiers
  MAP = {'GC073': ['C3', 'D3', 'E3'],
         'GC069': ['C4', 'D4', 'E4'],
         'GC075': ['C5', 'D5', 'E5'],
         'GC070': ['C6', 'D6', 'E6'],
         'GC065': ['C7', 'D7', 'E7'],
         'GC098': ['C8', 'D8', 'E8'],
         'GC074': ['C9', 'D9', 'E9'],
         'GC097': ['C10', 'D10' ,'E10'],
         'GC084': ['F3', 'F4', 'F5'],
         'GC106': ['F6', 'F7', 'F8'],
         'GC100': ['F9', 'F10', 'F11']} 
  
  # Generate a list of all valid wells
  wells = [f'{letter}{number}' for letter in ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H'] for number in np.arange(1,13)]
  
  # Load the data
  data = pd.read_csv(f'{ROOT}/data/growth_rates/{DATE}_r{RUN_NO}_{STRAINS}_{MEDIUM}/{DATE}_r{RUN_NO}.csv', 
                  skiprows=SKIPROWS)
  
  # Melt and drop unnecessary stuff
  melted = data.melt(id_vars=['Time'], var_name='well', value_name='od_600nm')
  melted = melted.loc[melted['well'].isin(wells)]
  melted.dropna(inplace=True)
  
  # Add strain identifier and replicates
  melted['strain'] = 'blank'
  melted['replicate'] = 0
  for strain, wells in MAP.items():
      for idx, well in enumerate(wells):
          melted.loc[melted['well']==well, 'strain'] = strain
          melted.loc[melted['well']==well, 'replicate'] = idx + 1
  
  # Add information regarding date and growth medium
  melted['growth_medium'] = MEDIUM
  melted['date'] = DATE
  melted['run_number'] = RUN_NO
  
  # Convert time to elapsed time
  melted['time_sec'] = pd.to_timedelta(melted['Time'].values)
  melted['time_sec'] = melted['time_sec'].dt.total_seconds()
  melted['elapsed_time_hr'] = (melted['time_sec'] - melted['time_sec'].min())/3600
  
  # Drop unnecessary Time columns
  melted.drop(columns=['Time', 'time_sec'], inplace=True)
  
  
  # Reformat blank value as average eentry per time
  measurement = []
  for g, d in melted.groupby(['elapsed_time_hr']):
      d = d.copy()
      avg_blank = d[d['strain']=='blank']
      meas = d[d['strain']!='blank']
      meas['avg_blank_value'] = avg_blank['od_600nm'].mean()
      measurement.append(meas)
  measurement = pd.concat(measurement, sort=False)
  measurement.rename(columns={'strain':'identifier'}, inplace=True)
  
  # Add shorthand strain information and class identifier
  strain_shorthand, _, strain_class = futileprot.io.standardize_strains(measurement['identifier'].values)
  measurement['strain'] = strain_shorthand
  measurement['class'] = strain_class
  
  # measurement = pd.concat(measurements, sort=False)
  # Save to disk
  measurement.to_csv(f'./output/{DATE}_r{RUN_NO}_{STRAINS}_{MEDIUM}_measurements.csv', index=False)
  
  #%%
  # Perform the blank subtraction
  measurement['od_600nm_subtracted'] = measurement['od_600nm'].values - measurement['avg_blank_value'].values
  
  # Given truncation, recalculated elapsed time and save truncated data
  trunc = []
  for g, d in measurement.groupby(['strain', 'replicate']):
      d = d.copy()
      d = d[(d['od_600nm_subtracted'] >= OD_BOUNDS[0]) & 
            (d['od_600nm_subtracted'] <= OD_BOUNDS[1])]
      d['elapsed_time_hr'] -= d['elapsed_time_hr'].min()
      trunc.append(d)
  trunc = pd.concat(trunc, sort=False)
  trunc = trunc[['strain', 'elapsed_time_hr', 
               'od_600nm_subtracted', 'replicate', 'growth_medium', 
               'date', 'run_number', 'identifier', 'class']]
  trunc.rename(columns={'od_600nm_subtracted':'od_600nm',
                        'replicate':'technical_replicate'}, inplace=True)
  trunc.to_csv(f'./output/{DATE}_r{RUN_NO}_{STRAINS}_{MEDIUM}_exponential_phase.csv', index=False)
  
  # %%
  # Generate a figure of all of the raw traces
  raw_traces = alt.Chart(
                      data=measurement, 
                      width=400, 
                      height=200
                  ).mark_line(
                      point=True,
                      opacity=0.75
                  ).encode(
                      x=alt.X('elapsed_time_hr:Q', title='elapsed time [hr]'),
                      y=alt.Y('od_600nm:Q', title='optical density [a.u.]'),
                      color=alt.Color('replicate:N', title='technical replicate')
                  ).facet(
                      row='strain'
                  )
  altair_saver.save(raw_traces, f'output/{DATE}_r{RUN_NO}_{STRAINS}_{MEDIUM}_raw_traces.png',
                   scale_factor=2)
  
  # %%
   #%%
  import numpy as np 
  import pandas as pd 
  import futileprot.viz
  import altair as alt
  import altair_saver
  import scipy.stats
  colors, palette = futileprot.viz.altair_style()
  
  # Add metadata
  DATE = '2021-07-27'
  RUN_NO = 2
  STRAINS = 'SingleKO'
  MEDIUM = 'acetate'
  
  # Load the measurement data
  data = pd.read_csv(f'./output/{DATE}_r{RUN_NO}_{STRAINS}_{MEDIUM}_exponential_phase.csv')
  
  # Perform a simplistic inference of the growth rate to get a sense of what
  # the result is.
  # data = data.groupby(['strain', 'elapsed_time_hr']).mean().reset_index()
  data = data[['strain', 'elapsed_time_hr', 'od_600nm']]
  
  # For each strain, infer the growth rate and compute the fit
  layout = False
  for g, d in data.groupby(['strain']):
      time_range = np.linspace(0, 1.25 * d['elapsed_time_hr'].max(), 10)
  
      # Perform the regression
      popt = scipy.stats.linregress(d['elapsed_time_hr'], np.log(d['od_600nm']))
      slope, intercept, err = popt[0], popt[1], popt[-1]
      print(f'{g}, {MEDIUM}: µ = {slope:0.3f} ± {err:0.3f} per hr.')
      # Compute the fit
      fit = np.exp(intercept + slope * time_range)
      fit_df = pd.DataFrame([])
      fit_df['elapsed_time_hr'] = time_range
      fit_df['od_600nm'] = fit
  
      # Generate the plot
      points = alt.Chart(
                          data=d, 
                          width=300,  
                          height=150
                      ).mark_point(
                          color=colors['primary_blue']
                      ).encode(
                          x=alt.X('elapsed_time_hr:Q', title='elapsed time [hr]'),
                          y=alt.Y('od_600nm:Q', title='optical density [a.u]',
                                  scale=alt.Scale(type='log'))
                      )
  
      fit = alt.Chart(data=fit_df,
                      title=f'{g}, {MEDIUM}: µ = {slope:0.3f} ± {err:0.3f} per hr.'
                      ).mark_line(   
                          color=colors['primary_blue']
                      ).encode(
                          x='elapsed_time_hr:Q',
                          y='od_600nm:Q'
                      )
      merge = points + fit
      if layout == False:
          layout = merge
      else: 
          layout &= merge
  
  altair_saver.save(layout, f'output/{DATE}_r{RUN_NO}_{STRAINS}_{MEDIUM}_fits.png',
                  scale_factor=2)
  # %%
  
apis: pandas.read_csv pandas.DataFrame numpy.log numpy.exp
apis: pandas.read_csv pandas.to_timedelta numpy.arange pandas.concat
apis: pandas.read_csv pandas.DataFrame numpy.log numpy.exp