import numpy as np
import netCDF4 as nc
import pandas as pd
from operator import mul
from files import *
import os
import re

def pre_process(files, year, mode="basic"):
	"""
	A tedious pre-processing function that prepares data for plotting
	"""
	# pull cohort data
	rest_fin = files.restart[year]
	param_fin = files.param
	crown_area = carea_allom(rest_fin, param_fin)
	cohort_height = rest_fin.variables['fates_height'][:]
	pft = rest_fin.variables['fates_pft'][:]
	canopy_layer = rest_fin.variables['fates_canopy_layer'][:]
	trunk_width = rest_fin.variables['fates_dbh'][:]
	num_plants = rest_fin.variables['fates_nplant'][:]

	# pull patch data
	patch_area = rest_fin.variables['fates_area'][:]
	patch_area_clean = [area if area else np.nan for area in patch_area] # fills empty values with NaN
	patch_age = rest_fin.variables['fates_age'][:]
	patch_age_clean = [age if age else np.nan for age in patch_age] # fills empty values with NaN

	# make dataframe1: combined cohort and patch data
	data1 = {
		'crown_area':crown_area,
		'cohort_height': cohort_height,
		'pft': pft,
		'canopy_layer': canopy_layer,
		'trunk_width': trunk_width,
		'patch_area': patch_area_clean,
		'patch_age': patch_age_clean,
		'num_plants': num_plants*1600}
	df1 = pd.DataFrame(data1)
	df1[['patch_area', 'patch_age']] = df1[['patch_area', 'patch_age']].ffill() # fills NaN values
	df1 = df1[df1['cohort_height'] != 0] # gets rid of empty cohorts
	df1['canopy_layer'] = df1['canopy_layer'].replace({1: 'orange', 2: 'grey'})

	# make dataframe2: a master df that only has patch data
	# clean data
	patch_area_c = [area for area in patch_area if area] # gets rid of 0's
	patch_age_c = [age for age in patch_age if age] # gets rid of 0's
	data2 = {'patch_area': patch_area_c, 'patch_age': patch_age_c}
	try:
		df2 = pd.DataFrame(data2)
	except ValueError:
		patch_age_c.extend([0]*(len(patch_area_c)-len(patch_age_c)))
		df2 = pd.DataFrame(data2)

	# merge small areas
	small = df2[df2['patch_area'] <= find_small(df2, 1000)]
	small_areas = small['patch_area'].tolist()
	small_ages = small['patch_age'].tolist()
	sum_small_areas = mul([sum(small_areas)], len(small_areas))
	avg_small_ages = mul([np.average(small_ages)], len(small_ages))
	for df in [df1, df2]:
		df['patch_age'] = df['patch_age'].replace(to_replace=small_ages, value = avg_small_ages)
		df['patch_area'] = df['patch_area'].replace(to_replace = small_areas, value = sum_small_areas)
	df2 = df2.drop_duplicates(keep='first')

	# calculate coverage by patch
	coverages = []
	for patch in df2['patch_area'].tolist():
		to_sum = df1[df1['patch_area'] == patch]
		coverages.append(find_individual_coverage(to_sum, patch))
	df2 = df2.assign(coverages = coverages)

	# sort data by height
	df1 = df1.sort_values(['patch_area','cohort_height'])
	df2 = df2.sort_values('patch_area')
	# make data for plotting canopy and stem
	df1['canopy_bottom'] = df1['cohort_height'] * 0.6
	df1['canopy_width'] = df1['crown_area'] / (df1['cohort_height'] * 0.4)
	results = []
	for area in df2['patch_area'].to_list():
		patch = df1[df1['patch_area'] == area]['canopy_width'].to_list()
		patch = np.asarray(patch) + 10
		cumsum = np.cumsum(patch)
		processed = [a - (b/2) for a, b in zip(cumsum, patch)]
		results.extend(processed)
	df1['stem_location'] = results
	df1['year'], df2['year'] = year, year

	if mode == "basic":
		return df1, df2
	# create a simplified version of the data
	areas = df2['patch_area'].tolist()
	simplified_patches = pd.DataFrame()
	for area in areas:
		target = df1[df1['patch_area'] == area]
		max_dbh = max(target['trunk_width'])
		target = target.assign(dbh_binned = pd.cut(target['trunk_width'],
								np.array([0,5,10,15,20,30,40,50,60,70,80,90,100,max(101,max_dbh)]),
								labels = ['SC1', 'SC2', 'SC3', 'SC4', 'SC5', 'SC6', 'SC7', 'SC8',
										 'SC9', 'SC10', 'SC11', 'SC12', 'SC13']))
		grouped = target.groupby(['pft', 'canopy_layer', 'dbh_binned']).agg(
					trunk_width = ('trunk_width', np.median),
					num_plants=('num_plants', 'sum'),
					cohort_height = ('cohort_height', np.median),
					crown_area = ('crown_area', 'sum'),
					patch_area = ('patch_area', 'first'),
					patch_age = ('patch_age', 'first'))
		grouped = grouped.reset_index().dropna()
		grouped = grouped.assign(canopy_bottom = grouped['cohort_height'] * 0.6 )
		grouped = grouped.assign(canopy_width = grouped['crown_area'] / (grouped['cohort_height'] * 0.4))
		grouped = grouped.sort_values(by=['canopy_layer', 'pft'])
		patch = grouped['canopy_width'].to_list()
		patch = np.asarray(patch) + 10
		cumsum = np.cumsum(patch)
		processed = [a - (b/2) for a, b in zip(cumsum, patch)]
		grouped = grouped.assign(stem_location = processed)
		simplified_patches = simplified_patches.append(grouped, sort=True)
		simplified_patches['year'] = year

	if mode == "patch simplified":
		return simplified_patches, df2
	elif mode == "one patch":
		simplified_patches.sort_values(by="cohort_height", inplace=True)
		index = simplified_patches["canopy_width"].tolist()
		patch = np.array(index) + 10
		index = np.cumsum(patch)
		index = [a - (b/2) for a, b in zip(index, patch)]
		simplified_patches["stem_location"] = index
		simplified_patches["patch_age"] = 0
		simplified_patches["patch_area"] = 1
		df2 = pd.DataFrame({"patch_age":[0], "patch_area": [1]})
		return simplified_patches, df2

def carea_allom(rest_fin, paramfin):
		"""REST_FIN: a restart file
		   PARAMFIN: a parameter file
		   Returns a list of crown area of each cohort?
		"""
		ncohorts_max = len(rest_fin.variables['fates_CohortsPerPatch'][:])
		# ncohorts_max: maximum number of cohorts (some of the rows in this variable are empty)
		# number of cohorts per patch
		pft = rest_fin.variables['fates_pft'][:]  # plant functional group
		dbh = rest_fin.variables['fates_dbh'][:]  # diameter at breast height
		carea = np.ma.masked_all(ncohorts_max)
		fates_allom_cmode = paramfin.variables['fates_allom_cmode'][:] # coarse root biomass allometry function index
		if np.any(fates_allom_cmode != 1):
			raise Exception
		carea_exp = paramfin.variables['fates_allom_d2bl2'][:]
		fates_allom_d2ca_coefficient_max = paramfin.variables['fates_allom_d2ca_coefficient_max'][:]
		fates_allom_d2ca_coefficient_min = paramfin.variables['fates_allom_d2ca_coefficient_min'][:]
		fates_allom_blca_expnt_diff = paramfin.variables['fates_allom_blca_expnt_diff'][:]
		fates_spread = rest_fin.variables['fates_spread'][:]
		fates_nplant = rest_fin.variables['fates_nplant'][:]
		fates_allom_dbh_maxheight = paramfin.variables['fates_allom_dbh_maxheight'][:]
		d2ca_coeff = fates_allom_d2ca_coefficient_max * fates_spread + fates_allom_d2ca_coefficient_min * (
					1. - fates_spread)
		for i in range(ncohorts_max):
			if pft[i] > 0:
				eff_dbh = np.min([dbh[i], fates_allom_dbh_maxheight[pft[i] - 1]])
				carea[i] = d2ca_coeff[pft[i] - 1] * (
							eff_dbh ** (carea_exp[pft[i] - 1] + fates_allom_blca_expnt_diff[pft[i] - 1])) * fates_nplant[i]
		return carea

# some helper functions
def find_max(self):
	"""find the max number of plants in a file upload"""
	for file in self.detailed:
		max_in_one_file = max(file['num_plants'].tolist())
		self.max_plants = max(self.max_plants, max_in_one_file)

def find_small(df, threshold):
	"""find the total area below THREAHOLD"""
	ranked = df.sort_values('patch_area')
	areas = ranked['patch_area'].tolist()
	total = 0
	maximum = 0
	while total + areas[0] <= threshold:
		total += areas[0]
		maximum = areas[0]
		areas = areas[1:]
	return maximum

def find_individual_coverage(df, total):
	grouped = df.groupby('canopy_layer', as_index=False)[['crown_area']].sum()
	try:
		canopy_raw = grouped[grouped['canopy_layer'] == "orange"].reset_index()['crown_area'][0]
	except (KeyError, IndexError):
		canpy_raw = 0
	try:
		understory_raw = grouped[grouped['canopy_layer'] == 'grey'].reset_index()['crown_area'][0]
	except (KeyError, IndexError):
		understory_raw = 0
	data = [canopy_raw, understory_raw, total - max(canopy_raw, understory_raw)]
	if data[2] < 0:
		data[2] = 0
	return data


# -*- coding: utf-8 -*-
# import
## batteries
from __future__ import print_function
import os
import sys
import argparse
from itertools import product,cycle
import string
## 3rd party
import numpy as np
import pandas as pd
## package
from pyTecanFluent import Utils
from pyTecanFluent import Fluent
from pyTecanFluent import Labware

# functions
def get_desc():
    desc = 'Create robot commands for qPCR assay setup'
    return desc

def parse_args(test_args=None, subparsers=None):
    # desc
    desc = get_desc()
    epi = """DESCRIPTION:
    Create a worklist file for the TECAN Fluent robot for qPCR setup.
    The input is an exported plate layout from the BioRad qPCR software.
    The file format should be Excel or CSV.
    Just create a plate layout for your experimet, then export and add some needed info:
      The following columns should also be added to the table:
      * "Sample labware name"
         * labware name containing the sample (any name that you want)
         * Exmaple: "source plate"
      * "Sample labware type"
         * labware type (must EXACTLY match an existing labware type)
         * Example: "96 Eppendorf TwinTec PCR"
      * "Sample location"
         * location of sample in the source plate.
         * numeric; column-wise indexing
      * "Sample volume"
         * numeric; sample volume in ul
      * "MM name"
         * Name of master mix for that sample
         * This allows for multiple master mixes per assay
      * "MM volume"
         * Volume of master mix in PCR rxn (ul)
      * "Water volume"
         * Volume of water in PCR rxn (ul)

    Notes:
    * Sample locations in plates numbered are column-wise (left-to-right)
    * The setup file (input table) MUST have a header (capitalization doesn't matter)
    * All volumes are in ul.
    """
    if subparsers:
        parser = subparsers.add_parser('qPCR', description=desc, epilog=epi,
                                       formatter_class=argparse.RawTextHelpFormatter)
    else:
        parser = argparse.ArgumentParser(description=desc, epilog=epi,
                                         formatter_class=argparse.RawTextHelpFormatter)

    # args
    ## I/O
    groupIO = parser.add_argument_group('I/O')
    groupIO.add_argument('setup', metavar='SetupFile', type=str,
                         help='An Excel or CSV file with experimental setup')
    groupIO.add_argument('--prefix', type=str, default='TECAN_qPCR',
                         help='Output file name prefix (default: %(default)s)')
    groupIO.add_argument('--format', type=str, default=None,
                         choices=[None, 'excel', 'csv', 'tsv'],
                         help='File format (excel, csv, or tsv). If not provided, the format is determined from the file extension (default: %(default)s)')

    ## Source labware
    src = parser.add_argument_group('Source labware')
    src.add_argument('--mm-type', type=str, default='2ml Eppendorf waste',
                      help='Mastermix labware type (default: %(default)s)')
    src.add_argument('--water-type', type=str, default='100ml_1 waste',
                      help='Water labware type (default: %(default)s)')

    ## Destination labware
    dest = parser.add_argument_group('Destination labware')
    dest.add_argument('--dest', type=str, default='Destination plate',
                      help='Destination plate labware name (default: %(default)s)')
    dest.add_argument('--dest-type', type=str, default='384 Well Biorad PCR',
                      help='Destination plate labware type (default: %(default)s)')

    # Liquid classes
    liq = parser.add_argument_group('Liquid classes')
    liq.add_argument('--mm-liq', type=str, default='MasterMix Free Multi Wall Disp',
                      help='Mastermix liquid class (default: %(default)s)')
    liq.add_argument('--samp-liq', type=str, default='Water Free Single Wall Disp',
                      help='Sample liquid class (default: %(default)s)')
    liq.add_argument('--water-liq', type=str, default='Water Free Single Wall Disp',
                      help='Water liquid class (default: %(default)s)')
    liq.add_argument('--n-tip-reuse', type=int, default=4,
                     help='Number of tip reuses for applicable reagents (default: %(default)s)')

    # Parse & return
    if test_args:
        args = parser.parse_args(test_args)
        return args

    return parser

def main(args=None):
    # Input
    if args is None:
        args = parse_args()
    check_args(args)

    # Load input assay setup table
    df_setup = load_setup(args.setup,
                          file_format=args.format)

    # gwl object init
    TipTypes = ['FCA, 1000ul SBS', 'FCA, 200ul SBS',
                'FCA, 50ul SBS', 'FCA, 10ul SBS']
    gwl = Fluent.gwl(TipTypes)

    # adding sample/reagent destinations to setup table
    n_wells = gwl.db.get_labware_wells(args.dest_type)
    add_dest(df_setup, args.dest, args.dest_type, n_wells)
    df_setup = check_rack_labels(df_setup)

    # Reordering dest for optimal pipetting
    if n_wells == 384:
        df_setup = Utils.reorder_384well(df_setup, gwl,
                                       labware_name_col='dest_labware_name',
                                       labware_type_col='dest_labware_type',
                                       position_col='dest_target_position')
    elif n_wells == 96:
        df_setup.sort(['dest_target_position'], inplace=True)
    else:
        msg = 'Labware type "{}" not recognized'
        raise ValueError(msg.format(args.desttype))

    # Adding commands to gwl object
    pip_mastermixes(df_setup, gwl=gwl,
                    src_labware_type=args.mm_type,
                    liq_cls=args.mm_liq,
                    n_tip_reuse=args.n_tip_reuse)

    ## Samples
    pip_samples(df_setup, gwl=gwl,
                liq_cls=args.samp_liq)

    ## Water
    pip_water(df_setup, gwl=gwl,
              src_labware_type=args.water_type,
              liq_cls=args.water_liq)

    ## writing out worklist (gwl) file
    gwl_file = args.prefix + '.gwl'
    gwl.write(gwl_file)

    # making labware table
    lw = Labware.labware()
    lw.add_gwl(gwl)
    lw_df = lw.table()
    lw_file = args.prefix + '_labware.txt'
    lw_df.to_csv(lw_file, sep='\t', index=False)

    # Creating report file
    report_file = args.prefix + '_report.txt'
    with open(report_file, 'w') as repFH:
        MM_names = np.unique(df_setup['mm name'])
        for i,MM_name in enumerate(MM_names):
            df = df_setup.loc[df_setup['mm name'] == MM_name]
            df.reset_index(inplace=True)
            write_report(df, MM_name=MM_name, outFH=repFH)

    # status on files written
    Utils.file_written(gwl_file)
    Utils.file_written(lw_file)
    Utils.file_written(report_file)

def check_args(args):
    """Checking user input
    """
    # special characters for namings
    args.dest = Utils.rm_special_chars(args.dest)

def load_setup(input_file, file_format=None, header=0):
    """Loading setup file (Excel, csv, or tab-delim)
    """
    # format
    if file_format is None:
        if input_file.endswith('.csv'):
            file_format = 'csv'
        elif input_file.endswith('.txt') or input_file.endswith('.tsv'):
            file_format = 'tab'
        elif input_file.endswith('.xls') or input_file.endswith('.xlsx'):
            file_format = 'excel'
    else:
        file_format = file_format.lower()

    # load via pandas IO
    if file_format == 'csv':
        df = pd.read_csv(input_file, sep=';', header=header)
    elif file_format == 'tab':
        df = pd.read_csv(input_file, sep='\t', header=header)
    elif file_format == 'excel':
        xls = pd.ExcelFile(input_file)
        df = pd.read_excel(xls, header=header)
    else:
        raise ValueError('Setup file not in usable format')

    # caps-invariant column IDs
    df.columns = [x.lower() for x in df.columns]

    # checking format of table
    check_df_setup(df)

    # filtering NaN for required columns
    df.dropna(subset=['sample type', 'sample labware name', 'sample location'],
              inplace=True)

    # making sure labware names are "TECAN worklist friendly"
    df = Utils.rm_special_chars(df, 'sample labware name')

    # assert & return
    assert df.shape[1] > 1, 'Input file is only 1 column; wrong delimiter used?'
    return df

def check_df_setup(df_setup):
    """Assertions of df_conc object formatting
    """
    # checking for column IDs
    col_IDs = ('row', 'column', 'sample type',
               'sample labware name', 'sample labware type',
               'sample location', 'sample volume',
               'mm name', 'mm volume', 'water volume')
    msg = 'Column "{}" not found (captilization invariant)'
    for x in col_IDs:
        if not x in df_setup.columns:
            raise ValueError(msg.format(x))

    # checking sample locations (>=1)
    msg = 'ERROR (SetupFile, line={}): location is < 1'
    for i,loc in enumerate(df_setup['sample location']):
        if loc < 1:
            print(msg.format(i), file=sys.stderr)

    # checking sample conc
    msg = 'ERROR (setupFile, line={}): volume is < 0'
    for i,vol in enumerate(df_setup['sample volume']):
        assert np.isnan(vol) or vol >= 0.0, msg.format(i)
    for i,vol in enumerate(df_setup['mm volume']):
        assert np.isnan(vol) or vol >= 0.0, msg.format(i)
    for i,vol in enumerate(df_setup['water volume']):
        assert np.isnan(vol) or vol >= 0.0, msg.format(i)

    # removing "tube" from end of labware type (if present)
    Utils.rm_tube(df_setup, 'sample labware type')

def check_rack_labels(df_setup):
    """Removing '.' for rack labels (causes execution failures)
    """
    cols = ['sample labware name', 'mm name', 'dest_labware_name']
    for x in cols:
        df_setup[x] = [str(y).replace('.', '_') for y in df_setup[x].tolist()]
    return df_setup

def plate2robot_loc(row_val, col_val, n_wells):
    """Changing positioning from row (letter) and column (number)
    to just numeric position (column-wise) on the plate,
    which is needed for TECAN robot positioning.
    Using index for identifying well number in plate
    [args]
    row_val: string
    col_vol: string
    plate_type: string; plate type to determine well location indexing
    """
    # index for converting row to numeric
    idx = string.ascii_uppercase
    idx = {x:i+1 for i,x in enumerate(idx)}
    row_val = idx[row_val]

    # getting location on plate
    msg = 'Destination location "{}" is out of range'
    if n_wells == 96:
        loc = (col_val - 1) * 8 + row_val
        assert loc > 0 and loc <= 96, msg.format(loc)
    elif n_wells == 384:
        loc = (col_val - 1) * 16 + row_val
        assert loc > 0 and loc <= 384, msg.format(loc)
    else:
        msg = 'Number of wells is not valid: "{}"'
        raise ValueError(msg.format(plate_type))
    return loc

def add_dest(df_setup, dest_labware_name, dest_labware_type, n_wells=96):
    """Setting destination locations for samples & reagents
    Adding to df_conc:
      [dest_labware, dest_location]
    """
    # setting destination labware
    df_setup['dest_labware_name'] = dest_labware_name
    df_setup['dest_labware_type'] = dest_labware_type

    # setting destination location based on plate layout
    func = lambda x: plate2robot_loc(x['row'], x['column'], n_wells=n_wells)
    df_setup['dest_target_position'] = df_setup.apply(func, 1)

def reorder_384well(df, reorder_col):
    """Reorder values so that the odd, then the even locations are
    transferred. This is faster for a 384-well plate
    df: pandas.DataFrame
    reorder_col: column name to reorder
    """
    df['TECAN_sort_IS_EVEN'] = [x % 2 == 0 for x in df[reorder_col]]
    df.sort_values(by=['TECAN_sort_IS_EVEN', reorder_col], inplace=True)
    df = df.drop('TECAN_sort_IS_EVEN', 1)
    df.index = range(df.shape[0])
    return df

def pip_mastermixes(df_setup, gwl, src_labware_type,
                    liq_cls='Mastermix Free Single',
                    n_tip_reuse=1):
    """Writing worklist commands for aliquoting mastermix.
    Re-using tips
    """
    # split by mastermix names (different master mixes)
    MM_names = np.unique(df_setup['mm name'])

    gwl.add(Fluent.Comment('Mastermixes'))
    for i,MM_name in enumerate(MM_names):
        # partitioning to just focal mastermix
        df = df_setup.loc[df_setup['mm name'] == MM_name]
        df.reset_index(inplace=True)

        # all same volumes for mastermix?
        pip_mastermix(df, gwl=gwl,
                      MM_name=MM_name,
                      src_labware_type=src_labware_type,
                      liq_cls=liq_cls,
                      n_tip_reuse=n_tip_reuse)

def pip_mastermix(df_map, gwl, MM_name, src_labware_type,
                  liq_cls='Mastermix Free Single',
                  n_tip_reuse=1):
    """Dispense of particular mastermix
    """
    # df copy
    df = df_map.copy()
    ## ordering df for proper tip reuse
    x = cycle(range(8))
    df['CHANNEL_ORDER'] = [next(x) for y in range(df.shape[0])]
    x = cycle(range(n_tip_reuse))
    df['TIP_BATCH'] = Utils.tip_batch(df['CHANNEL_ORDER'], n_tip_reuse)
    df.sort_values(by=['TIP_BATCH',
                       'CHANNEL_ORDER',
                       'dest_target_position'], inplace=True)
    df.reset_index(inplace=True)

    # iterating mastermix records in setup table (single mastermix)
    gwl.add(Fluent.Comment('Mastermix: {}'.format(MM_name)))
    for i in range(df.shape[0]):
        # aspiration
        asp = Fluent.Aspirate()
        asp.RackLabel = '{0} MM[{1:0>3}]'.format(MM_name, 1)
        asp.RackType = src_labware_type
        asp.Position = 1
        asp.Volume = df.loc[i,'mm volume']
        asp.LiquidClass = liq_cls
        gwl.add(asp)

        # dispensing
        disp = Fluent.Dispense()
        disp.RackLabel = df.loc[i,'dest_labware_name']
        disp.RackType = df.loc[i,'dest_labware_type']
        disp.Position = df.loc[i,'dest_target_position']
        disp.Volume = df.loc[i,'mm volume']
        asp.LiquidClass = liq_cls
        gwl.add(disp)

        # waste
        if (i + 1) % n_tip_reuse == 0 or i + 1 == df.shape[0]:
            gwl.add(Fluent.Waste())

    # finish section
    gwl.add(Fluent.Break())

def pip_mastermix_multi_disp(df, gwl, MM_name, src_labware_type, multi_disp=6,
                             liq_cls='Mastermix Free Multi'):
    """Writing worklist commands for aliquoting mastermix.
    Re-using tips
    """
    # assertions
    cols = ['dest_labware_name', 'dest_labware_type']
    assert df.drop_duplicates(cols).shape[0] == 1

    # getting wells to exclude
    lw_type = df.loc[0,'dest_labware_type']
    n_wells = gwl.db.get_labware_wells(lw_type)
    all_wells = [x+1 for x in range(n_wells)]
    target_pos = df['dest_target_position'].tolist()
    to_exclude = set(all_wells) - set(target_pos)

    # creating reagnet distribution command
    rd = Fluent.Reagent_distribution()
    rd.SrcRackLabel = '{0} MM[{1:0>3}]'.format(MM_name, 1)
    rd.SrcRackType = '1.5ml Eppendorf waste'
    rd.SrcPosStart = 1
    rd.SrcPosEnd = 1
    # dispense parameters
    rd.DestRackLabel = df.loc[0,'dest_labware_name']
    rd.DestRackType = df.loc[0,'dest_labware_type']
    rd.DestPosStart = 1
    rd.DestPosEnd = n_wells
    # other
    rd.Volume = df.loc[0,'mm volume']
    rd.LiquidClass = liq_cls
    rd.NoOfDiTiReuses = 2
    rd.NoOfMultiDisp = multi_disp
    rd.Direction = 0
    rd.ExcludedDestWell = ';'.join([str(x) for x in list(to_exclude)])
    # adding to gwl object
    gwl.add(rd)

    # adding break
    gwl.add(Fluent.Break())

def pip_samples(df_setup, gwl, liq_cls='Water Contact Wet Single'):
    """Commands for aliquoting samples into distination plate
    """

    gwl.add(Fluent.Comment('Samples'))
    # filtering 'nan' from table
    x = pd.notnull(df_setup['sample labware name'])
    y = pd.notnull(df_setup['sample labware type'])
    z = pd.notnull(df_setup['sample location'])
    df = df_setup.loc[x & y & z]
    df.reset_index(inplace=True)
    if df.shape[0] < df_setup.shape[0]:
        msg = 'WARNING: some samples skipped due to missing values!'
        print(msg, file=sys.stderr)

    # for each Sample, create asp/dispense commands
    for i in range(df.shape[0]):
        # aspiration
        asp = Fluent.Aspirate()

        asp.RackLabel = df.loc[i,'sample labware name']
        asp.RackType = df.loc[i,'sample labware type']
        asp.Position = df.loc[i, 'sample location']
        asp.Volume = df.loc[i,'sample volume']
        asp.LiquidClass = liq_cls
        gwl.add(asp)

        # dispensing
        disp = Fluent.Dispense()
        disp.RackLabel = df.loc[i,'dest_labware_name']
        disp.RackType = df.loc[i,'dest_labware_type']
        disp.Position = df.loc[i,'dest_target_position']
        disp.Volume = df.loc[i,'sample volume']
        asp.LiquidClass = liq_cls
        gwl.add(disp)

        # waste (no tip re-use)
        gwl.add(Fluent.Waste())

    gwl.add(Fluent.Break())

def pip_water(df_setup, gwl, src_labware_type,
              liq_cls='Water Contact Wet Single'):
    """Writing worklist commands for aliquoting water
    Using single asp-disp.
    """
    gwl.add(Fluent.Comment('Water'))

    # filtering 'nan' from table
    x = pd.notnull(df_setup['water volume'])
    df = df_setup.loc[x]
    df.index = range(df.shape[0])
    if df.shape[0] < df_setup.shape[0]:
        msg = 'WARNING: water asp/disp for some samples skipped due to missing "water volume" values!'
        print(msg, file=sys.stderr)

    # for each Sample, create asp/dispense commands
    for i in range(df.shape[0]):
        if df.loc[i,'water volume'] <= 0:
            msg = 'WARNING: skipping water asp/disp for sample (volue <= 0)'
            print(msg, file=sys.stderr)
            continue

        # aspiration
        asp = Fluent.Aspirate()
        asp.RackLabel = 'Water source[{0:0>3}]'.format(1)
        asp.RackType = src_labware_type
        asp.Position = 1
        asp.Volume = df.loc[i,'water volume']
        asp.LiquidClass = liq_cls
        gwl.add(asp)

        # dispensing
        disp = Fluent.Dispense()
        disp.RackLabel = df.loc[i,'dest_labware_name']
        disp.RackType = df.loc[i,'dest_labware_type']
        disp.Position = df.loc[i,'dest_target_position']
        disp.Volume = df.loc[i,'water volume']
        asp.LiquidClass = liq_cls
        gwl.add(disp)

        # waste (no tip re-use)
        gwl.add(Fluent.Waste())

    gwl.add(Fluent.Break())

def add_error(x, error_perc):
    if x is None:
        return None
    return x * (1.0 + error_perc / 100.0)

def write_report_line(outFH, subject, volume, round_digits=1, error_perc=None):
    if volume is None:
        v = 'NA'
    else:
        if error_perc is not None:
            volume = add_error(volume, error_perc)
        v = round(volume, round_digits)
    outFH.write('{}:\t{}\n'.format(subject, v))

def write_report(df_setup, MM_name, outFH):
    """Writing a report on the qPCR setup
    """
    # calculating total volumes
    n_rxn = df_setup.shape[0]
    ## total mastermix
    total_mm_volume = np.sum(df_setup['mm volume'])
    ## total water
    total_water_volume = np.sum(df_setup['water volume'])

    # report
    # number of samples
    outFH.write('# PCR REPORT\n')
    outFH.write('MasterMix: {}\n'.format(MM_name))
    outFH.write('Number of total rxns:\t{}\n'.format(n_rxn))
    ## raw total volumes
    outFH.write('# Total reagent volumes (ul)\n')
    write_report_line(outFH, 'MasterMix', total_mm_volume)
    write_report_line(outFH, 'Water', total_water_volume)
    ## with pipetting error
    outFH.write('# Total reagent volumes + 10% extra (ul)\n')
    write_report_line(outFH, 'MasterMix', total_mm_volume, error_perc=10)
    write_report_line(outFH, 'Water', total_water_volume, error_perc=10)
    ## end
    outFH.write('\n')

# main
if __name__ == '__main__':
    pass


def main():
    point = []
    for i in range(9):
        S = list(input())
        for j, s in enumerate(S):
            if s == "#":
                point.append([i, j])
    count = 0
    for i0 in range(len(point) - 3):
        for i1 in range(i0 + 1, len(point) - 2):
            for i2 in range(i1 + 1, len(point) - 1):
                d0 = distance(point[i0], point[i1])
                d1 = distance(point[i0], point[i2])
                d2 = distance(point[i1], point[i2])
                if not judge3([d0, d1, d2]):
                    continue
                for i3 in range(i2 + 1, len(point)):
                    d3 = distance(point[i0], point[i3])
                    d4 = distance(point[i1], point[i3])
                    d5 = distance(point[i2], point[i3])
                    flag = judge4([d0, d1, d2, d3, d4, d5])
                    if flag:
                        count += 1

    print(count)

def distance(a: list, b: list) -> int:
    x0, y0 = a
    x1, y1 = b
    d = (x1 - x0) ** 2 + (y1 - y0) ** 2
    return d

def judge3(dists: list) -> bool:
    d0, d1, d2 = sorted(dists)
    if d0 == d1:
        if d2 == 2 * d0 or d2 == d0:
            return True
    if d1 == d2:
        if d1 == 2 * d0:
            return True
    return False

def judge4(dists: list) -> bool:
    d0, d1, d2, d3, d4, d5 = sorted(dists)
    if d0 == d1 == d2 == d3:
        if d4 == d5:
            if d4 == 2 * d0:
                return True
    return False

if __name__ == "__main__":
    main()


import cv2
import numpy as np

class ObjectTracker:
    def __init__(self):
        self.trackers = None
        self.boxes = None

    def reset_trackers(self):
        # grab the appropriate object tracker using our dictionary of
        # OpenCV object tracker objects
        trackers = cv2.MultiTracker_create()
        self.trackers = trackers
        return trackers

    def add_new_trackers(self, current_obs, boxes, track_algo='csrt'):
        for box in boxes:
            self.add_new_tracker(current_obs, box, track_algo)

    def add_new_tracker(self, current_obs, box, track_algo='csrt'):
        # initialize a dictionary that maps strings to their corresponding
        # OpenCV object tracker implementations
        OPENCV_OBJECT_TRACKERS = {
            "csrt": cv2.TrackerCSRT_create,
            "kcf": cv2.TrackerKCF_create,
            "boosting": cv2.TrackerBoosting_create,
            "mil": cv2.TrackerMIL_create,
            "tld": cv2.TrackerTLD_create,
            "medianflow": cv2.TrackerMedianFlow_create,
            "mosse": cv2.TrackerMOSSE_create
        }
        new_tracker = OPENCV_OBJECT_TRACKERS[track_algo]()
        self.trackers.add(new_tracker, current_obs, box)

    def _get_bounding_box(self, obs, color):
        """ Calculates the bounding box of a ndarray"""
        mask = np.all(obs == color, axis=-1)
        rows = np.any(mask, axis=0)
        cols = np.any(mask, axis=1)

        if len(np.where(rows)[0]) == 0 or len(np.where(cols)[0]) == 0:
            return None
        rmin, rmax = np.where(rows)[0][[0, -1]]
        cmin, cmax = np.where(cols)[0][[0, -1]]

        return [rmin, cmin, rmax-rmin, cmax-cmin]

    def _get_key_pickup_bounding_box(self, obs):
        key_pickup_agent_color = (127, 127, 127)
        key_pickup_key_color = (0, 0, 255)
        key_pickup_door_color = (0, 0, 0)

        boxes = []
        for color in [key_pickup_agent_color, key_pickup_key_color, key_pickup_door_color]:
            box = self._get_bounding_box(obs, color)
            if box is not None:
                boxes.append(box)
        return boxes

    def update(self, obs):
        (success, boxes) = self.trackers.update(obs)
        self.boxes = boxes
        return success, boxes

    def add_bounding_boxes(self, obs, boxes):
        # no need to call update fn again. just use the boxes.
        for box in boxes:
            (x, y, h, w) = [int(v) for v in box]
            cv2.rectangle(obs, (x, y), (x + w, y + h), (0, 255, 0), 2)

        # as numpy
        return obs


from .classes import LinkedList

def swap_halves(lst):
    # If we have on;y one element, we do not need to swap anything
    if lst.head.next is None:
        raise ValueError("Too short list (must have more than one element)")

    # Set pointers to first and second node
    middle_node = lst.head
    last_node = lst.head.next

    # Every move middle_node onto the next one every two moves of last_node
    # We assume that we will always have even number ove elements, to make things easier
    even = False
    while last_node.next is not None:
        last_node = last_node.next
        if even:
            middle_node = middle_node.next
            even = not even
        else:
            even = not even

    # last node now should point at the head
    last_node.next = lst.head

    # when the new head is the next element after middle
    lst.head = middle_node.next

    # And do not forget to empty last pointer or we will get infinite loop
    middle_node.next = None
    return lst

if __name__ == "__main__":
    lst = LinkedList([1, 2, 3, 4, 5, 6, 7, 8])
    print(swap_halves(lst))


from django.contrib import admin
from .models import User, Client, Contrat, Event
from django.contrib.auth.admin import UserAdmin
from django.contrib.admin import ModelAdmin

class UserAdminConfig(UserAdmin):
    ordering = ('-start_date',)
    list_display = ('email', 'username', 'first_name', 'groups',
                    'is_active', 'is_staff')
    list_filter = ('groups', 'is_active')
    search_fields = ('email', 'username', 'groups')
    fieldsets = [
        ('User Information',
         {
             'fields': [('email', 'username'), ('first_name', 'last_name')], }, ),
        ('Permissions',
         {
             'fields': ['is_staff', 'is_active', 'groups'], }, ),
    ]
    add_fieldsets = (
        ('User Information',
         {'classes': ('wide',),
          'fields': (
              'email', 'username', 'first_name', 'password1', 'password2'), },),
        ('Groups',
         {'fields': (
             'groups', 'is_active', 'is_staff',), },),
    )
    filter_horizontal = ()
    radio_fields = {'groups': admin.VERTICAL}

    def save_model(self, request, obj, form, change):
        """
            Check if each competition are already passed or no
            & add keyword 'valid' set on True or False

            :param: competitions: list of competitions
            :return: list of competitions
        """
        if change:
            saler = User.objects.filter(email=obj.email)
            old_team = saler[0].groups
            if old_team != obj.groups:
                clients = Client.objects.filter(sales_contact__in=saler)
                for client in clients:
                    client.sales_contact = None
                    client.save()
        super().save_model(request, obj, form, change)

class ClientAdminConfig(ModelAdmin):
    ordering = ('-date_created',)
    list_display = ('company_name', 'email', 'sales_contact', 'client_status', 'date_created', 'date_updated')
    list_filter = ('sales_contact', 'client_status')
    search_fields = ('company_name', 'email', 'sales_contact')

    fieldsets = [
        ('Client Information',
         {
             'fields': ['company_name', ('first_name', 'last_name')], },),
        ('Contact',
         {
             'fields': ['email', 'phone', 'sales_contact', 'client_status'], },),
    ]
    radio_fields = {'client_status': admin.VERTICAL}

    @admin.action(description="client status = Existant")
    def client_status_ex(self, request, query):
        query.update(client_status="EX")

    @admin.action(description="client status = Potentiel")
    def client_status_po(self, request, query):
        query.update(client_status="PO")

    actions = [client_status_ex, client_status_po]

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == 'sales_contact':
            kwargs['queryset'] = User.objects.filter(groups__name__contains='sales')
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

class EventAdminConfig(ModelAdmin):
    ordering = ('-date_created',)
    list_display = ('event_name', 'attendees', 'event_date', 'support_contact', 'contrat', 'event_status')
    list_display_links = ('event_name', 'support_contact', 'contrat')
    list_filter = ('event_date', 'support_contact', 'event_status')
    search_fields = ('event_name',)

    fieldsets = [
        ('Event Information',
         {
             'fields': ['event_name', 'attendees', 'event_date'], },),
        ('Links',
         {
             'fields': ['contrat', 'support_contact'], },),
        ('More',
         {
             'fields': ['event_status', 'notes'], },),
    ]
    radio_fields = {'event_status': admin.VERTICAL}

    @admin.action(description="event status = Organisation")
    def event_status_or(self, request, query):
        query.update(event_status="OR")

    @admin.action(description="event status = Prêt")
    def event_status_pr(self, request, query):
        query.update(event_status="PR")

    @admin.action(description="event status = Terminé")
    def event_status_te(self, request, query):
        query.update(event_status="TE")

    actions = [event_status_te, event_status_or, event_status_pr]

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == 'support_contact':
            kwargs['queryset'] = User.objects.filter(groups__name__contains='support')
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

class ContratAdminConfig(ModelAdmin):
    ordering = ('-date_created',)
    list_display = ('internal_contrat_number', 'client', 'amount', 'payment_due', 'status')
    list_filter = ('client', 'status')
    search_fields = ('client',)

    fieldsets = [
        ('Contrat Information',
         {
             'fields': ['internal_contrat_number', 'amount', 'payment_due', 'status'], },),
        ('Links',
         {
             'fields': ['client'], },),
    ]

    def formfield_for_foreignkey(self, db_field, request, **kwargs):
        if db_field.name == 'client':
            kwargs['queryset'] = Client.objects.filter(client_status='EX')
        return super().formfield_for_foreignkey(db_field, request, **kwargs)

admin.site.register(User, UserAdminConfig)
admin.site.register(Client, ClientAdminConfig)
admin.site.register(Contrat, ContratAdminConfig)
admin.site.register(Event, EventAdminConfig)


from experiment import reciprocal_rank
import numpy as np

def random_classifier(batch_size, epochs):
    MRR = []
    for e in range(epochs):
        score = []
        probs = np.random.random((batch_size, batch_size))  # compute fake similarities -> go random!
        for i in range(batch_size):
            score.append(reciprocal_rank(probs[i], probs[i][0]))  # pretend GT is always in the first position
        MRR.append(np.mean(score))
    return np.mean(MRR)

if __name__ == '__main__':
    epochs = 1000
    batch_size = 64
    print("running Random Classifier with batch size {} for {} epoches and averaging.".format(batch_size, epochs))
    score = random_classifier(batch_size, epochs)
    print("average MRR score: {}".format(score))