import numpy as np
import matplotlib.pyplot as plt
import shutil
import os
import copy
import xml.etree.ElementTree as ET
import glob
import pdb
import time

import openmc
import openmc.mgxs as mgxs
from openbu.cell import Cell
from openbu.system import System
from openbu import salameche
from .openmc_fix import *

from openbu import utils
from openbu import data

class Couple_openmc(object):

	# One-group energy bin
	energy_bin = openmc.EnergyFilter([0., 20.0e6])

	# Multigroup energy bin
	minorder = -3
	maxorder = 7
	mg_energy = np.logspace(minorder, maxorder, (maxorder - minorder) * 30 + 1)
	mg_energy_mid_points = [(x+y)/2 for x,y in zip(mg_energy[1:],mg_energy[:-1])]
	#mg_energy = np.logspace(-3, 7, num=300, base=10.0)
	mg_energy_bin = openmc.EnergyFilter(mg_energy)

	zero_dens_1_atm = 1E-24

	def __init__(self, MC_input_path = None, xs_mode = 'no constant lib', MPI = None):

		# If no MC_input_path is input, it is set to cwd
		if MC_input_path == None:
			MC_input_path = os.getcwd()
		self._MC_input_path = MC_input_path

		# If no mode is input by the user, mode is set by default to 'const_lib'
		if xs_mode == None:
			xs_mode == 'constant lib'
		self._xs_mode = xs_mode

		# # If MPI is not set to on by the user, MPI is set to off
		# if MPI == None:
		# 	MPI == 'off'
		# self._MPI = MPI
		self._MPI = None

		self._volume_set = 'no'

		# List of selected cells to deplete
		self.selected_bucells_name_list = None
		# Dict of selected cells to deplete with their user defined nucl list
		self.selected_bucells_nucl_list_dict = None

		self._fy_lib_set = 'no'
		self._decay_lib_set = 'no'
		self._xs_lib_set = 'no'

		self._sampled_isomeric_branching_data = None
		self._sampled_ng_cross_section_data = None

		# This is the path set in OpenMC for the hdf5 point-wise cross sections
		self._cross_sections_path = None

		self._openmc_bin_path = None

		# Old way of defaulting MC_input_path to cwd
		# if args:
		# 	self._MC_input_path = arg[0]
		# else:
		# 	self._MC_input_path = os.getcwd()

# This method is used within the code to create a new material that is
#  uniquely associated to a particular cell

	@property
	def MC_input_path(self):

		return self._MC_input_path

	@property
	def xs_mode(self):
		return self._xs_mode

	@property
	def nucl_list_dict(self):

		return self._nucl_list_dict

	@property
	def root_cell(self):

		return self._root_cell

	@property
	def MPI(self):
		return self._MPI

	def set_MPI(self, execu, tasks):

		self._MPI = 'on'
		self._tasks = tasks
		self._exec = execu

	# for no_const_lib mode, defines the list of nucl that will be simulated
	# for mat id #
	# def set_nucl_list(self, mat_name, nucl_list):

	# 	self._nucl_list_dict[mat_name] = nucl_list

	def select_bucells(self, bucell_list):

		self.selected_bucells_name_list = []
		self.selected_bucells_nucl_list_dict = {}

		for arg in bucell_list:
			# If this element is a tuple (bucell , nucl list)
			if isinstance(arg,tuple):
				bucell_name = arg[0].name
				self.selected_bucells_name_list.append(bucell_name)
				self.selected_bucells_nucl_list_dict[bucell_name] = arg[1]
			else:
				self.selected_bucells_name_list.append(arg.name)

	def get_nucl_to_be_tallied(self, bucell):

		# If the user has provided a list of nuclide to be tallied
		if bucell.name in self.selected_bucells_nucl_list_dict:
			nucl_list_input = self.selected_bucells_nucl_list_dict[bucell.name]
			if nucl_list_input == 'initial nuclides':
				nucl_list = bucell.init_nucl
			elif nucl_list_input == 'NAX':
				NAX_nucl_list_name = utils.zamid_list_to_name_list(data.NAX_nucl_list)
				NAX_nucl_list_name_new_format = utils.bu_namelist_to_mc_namelist(NAX_nucl_list_name)
				# I add init_nucl because I believe it is important for init nuclides to be tallied
				# Their density is usually high enough that their change can influence spectrum etc...
				nucl_list = [x for x in NAX_nucl_list_name_new_format if x in self.MC_XS_nucl_list] + bucell.init_nucl
				# Here we remove the potential duplicates
				nucl_list = list(dict.fromkeys(nucl_list))
			else:
				nucl_list = nucl_list_input
		else:
			nucl_list = self.MC_XS_nucl_list

		return nucl_list

	# @property
	# def nucl_list(self):

	# 	return self._nucl_list

	# # for no_const_lib mode, defines a unique nuclide list for all materials
	# @nucl_list.setter
	# def nucl_list(self, nucl_list):

	# 	self._nucl_list = nucl_list

	# 	mat_dict = self.root_cell.get_all_materials()
	# 	for mat_id in mat_dict:
	# 		mat = mat_dict[mad_id]
	# 		mat_name = mat.name
	# 		self._nucl_list_dict[mat_id] = nucl_list

	@property
	def system(self):

		return self._system

	@system.setter
	def system(self, system):

		self._system = system

	def import_openmc(self, root_cell):

		#Instantiate a system
		system = System(1)
		self.system = system

		# read periodic surfaces  (openmc summary forgets the periodic surfaces coupling)
		# This function stores the periodic coupling of surfaces and it will be used later
		self._periodic_surfaces_dict = read_periodic_surfaces()

		# prerun to access cells and materials objects, to set cell volumes and if chosen
		# add 0 density nuclides
		self._pre_run(root_cell)

		bucell_dict = self.get_bucell_from_cell()
		system.bucell_dict = bucell_dict
		system.bounding_box = self.bounding_box

		# reads, modifies and set settings
		self._read_user_settings()

		# Move input files to input file folder
		self.gen_user_input_folder()
		self.copy_user_input()

		# MOVED TO OPENMC RUN
		# # New material xml file needs to be written with zero dens nuclides
		# self.export_material_to_xml()
		# # New geometry xml file needs to be written with new materials id
		# self.export_geometry_to_xml()
		# # Tallies xml files needs to be writen
		# self.export_tallies_to_xml()
		# # New settings xml files needs to be written
		# self.export_settings_to_xml()

	# Proably for when OpenBU pass dens to OpenMC

	@property
	def bounding_box(self):

		return self._bounding_box

	def set_bounding_box(self, ll, ur):

		self._bounding_box = [ll, ur]

	# def _set_material_nuclides(self, cell):

	# 	cell_id = cell.id
	# 	passlist = cell.passlists
	# 	total_dens = cell.total_dens

	# 	material = openmc.Material(cell_id)

	# 	material.set_density('atom/b-cm', total_dens)

	# 	for nuc in passlist:

	# 		nuc_name = nuc.name.replace('-', '')
	# 		nuc_ao = nuc.dens/total_dens
	# 		material.add_nuclide(nuc_name,  nuc_ao)

	def set_settings(self, settings, init_dist):
	# OpenMC simulation parameters
		batches = settings['batches']
		inactive = settings['inactive']
		particles = settings['particles']

		# Instantiate a Settings object
		settings_file = openmc.Settings()
		settings_file.batches = batches
		settings_file.inactive = inactive
		settings_file.particles = particles
		settings_file.output = {'tallies': True}

		# Create an initial uniform spatial source distribution over fissionable zones
		init_dist = setting.init_dist
		shape = init_dist['shape']
		low_left_bound = init_dist['low_left']
		up_right_bound = init_dist['up_right']
		if shape == 'Box':
			uniform_dist = openmc.stats.Box(low_left_bound, up_right_bound, only_fissionable=True)
		settings_file.source = openmc.source.Source(space=uniform_dist)

		# Export to "settings.xml"
		settings_file.export_to_xml()

	def gen_user_input_folder(self):

		utils.gen_folder('user_input')

	def copy_user_input(self):

		MC_input_path = self.MC_input_path
		user_input_folder_path = os.getcwd() + '/user_input'
		shutil.copyfile(MC_input_path + '/geometry.xml', user_input_folder_path + '/geometry.xml')
		shutil.copyfile(MC_input_path + '/materials.xml', user_input_folder_path + '/materials.xml')
		shutil.copyfile(MC_input_path + '/settings.xml', user_input_folder_path + '/settings.xml')

	# Probably obsolete

	# def get_summary(self):

	# 	MC_input_path = self.MC_input_path

	# 	get_summary_dir_path = MC_input_path +'/get_summary_dir'
	# 	os.mkdir(get_summary_dir_path)

	# 	# Instantiate a Settings object
	# 	settings_file = openmc.Settings()
	# 	settings_file.batches = 2
	# 	settings_file.inactive = 1
	# 	settings_file.particles = 8

	# 	# Copy the geometry and material file to the new dummy dir

	# 	shutil.copyfile(MC_input_path + '/geometry.xml', get_summary_dir_path + '/geometry.xml')
	# 	shutil.copyfile(MC_input_path + '/materials.xml', get_summary_dir_path + '/materials.xml')

	# 	# Export to "settings.xml"
	# 	settings_file.export_to_xml(path = get_summary_dir_path + '/settings.xml')

	# 	openmc.run(cwd = get_summary_dir_path)

	# 	summary = openmc.Summary(get_summary_dir_path + '/summary.h5')
	# 	# geo = summary.geometry
	# 	# cells = geo.get_all_cells()
	# 	shutil.rmtree(get_summary_dir_path)

	# 	return summary

	#def pre_read_xml(self):

		# To be able to launch the prerun, OpenBU needs at least to extract the list of
		# openmc cells and the boundingbox of the system
		# For that, the user needs to provide the

	def _pre_run(self, root_cell):

		MC_input_path = self.MC_input_path
		pre_run_path = os.getcwd() +'/pre_run'
		try:
			shutil.rmtree(pre_run_path)
		except OSError:
			pass
		os.mkdir(pre_run_path)

		# Prepare the volume calculation
		#bounding_box = root_cell.bounding_box
		ll = self.bounding_box[0]
		ur = self.bounding_box[1]
		cell_dict = root_cell.get_all_cells()
		cell_list = utils.cell_dict_to_cell_list(cell_dict)
		cell_list.append(root_cell) # Add root_cell so that the total volume is calculated
		vol1 = openmc.VolumeCalculation(cell_list, 100000, lower_left = ll, upper_right = ur)

		settings = openmc.Settings()
		settings.volume_calculations = [vol1]
		settings.temperature = {'method':'interpolation'}
		settings.run_mode='volume'
		settings.export_to_xml(path = pre_run_path + '/settings.xml')

		# Copy the geometry and material file to the new dummy dir
		shutil.copyfile(MC_input_path + '/geometry.xml', pre_run_path + '/geometry.xml')
		shutil.copyfile(MC_input_path + '/materials.xml', pre_run_path + '/materials.xml')

		# By default, the openm_exec is set to 'openmc'
		# For some reasons, this does not work on the cluster (della)
		# On della, we need to explicitly define the absolute path to the bin we want to use
		# Right now a temporary path that depends on my installation is used

		#openmc.calculate_volumes(cwd = pre_run_path, openmc_exec='/tigress/jdtdl/openmc/py3-mpi-190324/bin/openmc')
		openmc.calculate_volumes(cwd = pre_run_path, openmc_exec=self.openmc_bin_path)
		#openmc.run()

		# Read and set initial nuclides dict
		self.set_init_nucl_dict(root_cell)

		# # Read each material object and add 1atm nuclides chosen by the user
		# if self.mode == 'no_const_lib':
		# 	self.add_zero_dens_nuclides(self.nucl_list_dict)

		self._set_initial_summary(pre_run_path)
		self._set_cross_sections_path(pre_run_path)
		# Read cross sections xml files, create MC_XS_nucl_list
		self.set_MC_XS_nucl_list()
		self.set_root_universe()
		root_cell_name = 'root cell' # 	need to be specified by the user at some point
		self._set_root_cell(root_cell_name)

		# Extract cells from summary, add 1 atm nuclides to their material
		self._change_cell_materials()

		# Read and distribute volumes to cells
		self.set_vol_to_cell(vol1, pre_run_path)

		# pdb.set_trace()

		shutil.rmtree(pre_run_path)

	def set_vol_to_cell(self, vol1, pre_run_path):

		root_cell = self.root_cell
		cell_dict = root_cell.get_all_cells()
		cell_list = utils.cell_dict_to_cell_list(cell_dict)
		vol2 = vol1.from_hdf5(pre_run_path + '/volume_1.h5')
		for cell in cell_list:
			cell.add_volume_information(vol2)

		# Add volume for the root cell too and set it to system
		root_cell.add_volume_information(vol2)

		# This is now done in pass_vol when user does not define vol manually
		# system = self.system
		# system.total_vol = root_cell.volume

	# About summary
	# There are two type of OpenMC summary stored in openbu: initial_summary and updated_summary
	# I have noticed that updating the initial summary was causing some problem in OpenBU, i.e., the material xml file
	# would not update the density. The material was updating densities normaly when relying on the initial summary
	# A temporary solution for that is to separate the initial summary (that will be used to set dens to cells) and the
	# updated summary that will be used to extract densities to get 1g xs for bucells

	@property
	def initial_summary(self):

		return self._initial_summary

	def _set_initial_summary(self, path = os.getcwd()):

		initial_summary = openmc.Summary(path + '/summary.h5')

		######### OpenMC Summary src does not close the hdf5 file it opens
		######### When OpenBU tries to shutil.rmtree the pre_run folder, it can't because
		######### a stream to summary.h5 is still open
		######### We therefore close it here
		######### !!!! This should be modified in OpenMC at some points ###########
		initial_summary._f.close()
		######### !!!! This should be modified in OpenMC at some points ###########

		self._initial_summary = initial_summary

	@property
	def updated_summary(self):

		return self._updated_summary

	def _set_updated_summary(self, path = os.getcwd()):

		updated_summary = openmc.Summary(path + '/summary.h5')

		######### OpenMC Summary src does not close the hdf5 file it opens
		######### When OpenBU tries to shutil.rmtree the pre_run folder, it can't because
		######### a stream to summary.h5 is still open
		######### We therefore close it here
		######### !!!! This should be modified in OpenMC at some points ###########
		updated_summary._f.close()
		######### !!!! This should be modified in OpenMC at some points ###########

		self._updated_summary = updated_summary

	@property
	def statepoint(self):

		return self._statepoint

	def _set_statepoint(self, path = os.getcwd()):

		file_list = os.listdir()
		for file in file_list:
			if 'statepoint' in file:
				st_name = file
		statepoint = openmc.StatePoint(path + '/{}'.format(st_name))
		self._statepoint = statepoint

	def _set_kinf(self):

		statepoint = self.statepoint
		kinf = statepoint.k_combined

		system = self.system
		sequence = system.sequence
		sequence._set_macrostep_kinf(kinf)

	@property
	def root_cell(self):

		return self._root_cell

	def set_root_universe(self):

		summary = self.initial_summary
		geometry = summary.geometry
		root_universe = geometry.root_universe
		self._root_universe = root_universe

	def _set_root_cell(self, root_cell_name):

		summary = self.initial_summary
		# get_cells_by_name returns a list hence the index 0
		self._root_cell = summary.geometry.get_cells_by_name(root_cell_name)[0]

		add_periodic_surfaces(self._root_cell, self._periodic_surfaces_dict)

		region = self._root_cell.region
		#print (region.get_surfaces())
		for surface_id in region.get_surfaces():
			surface = region.get_surfaces()[surface_id]

	# While the most convenient way would be to extract path from summary.materials. It looks like
	# summary does not store the cross sections path in materials
	def _set_cross_sections_path(self, pre_run_path):

		path_to_materials_xml = pre_run_path + '/materials.xml'
		tree = ET.parse(path_to_materials_xml)
		root = tree.getroot()
		for child in root:
			if child.tag == 'cross_sections':
				self._cross_sections_path = child.text.replace('/cross_sections.xml', '')

	@property
	def materials(self):

		return self._materials

	def _change_cell_materials(self):

		summary = self.initial_summary
		materials = summary.materials
		for bucell_name in self.selected_bucells_name_list:
			# If bucell should only tally initial nuclide, no need to add 1 atm nuclides
			if self.selected_bucells_nucl_list_dict != {}:
				if bucell_name in self.selected_bucells_nucl_list_dict:
					if self.selected_bucells_nucl_list_dict[bucell_name] == 'initial nuclides':
						continue
			cell = summary.geometry.get_cells_by_name(bucell_name)[0]
			self.add_zero_dens_nuclides(cell)

	# def _change_cells_materials(self):

	# 	root_cell = self.root_cell
	# 	print (root_cell)
	# 	cell_dict = root_cell.get_all_cells()
	# 	# materials with 1atm nuclides
	# 	materials = self.materials
	# 	for cell_id in cell_dict:
	# 		cell = cell_dict[cell_id]
	# 		cell.get_all_materials
	# 		material_dict = cell.get_all_materials()
	# 		material = material_dict[list(material_dict.keys())[0]]
	# 		mat_name = material.name
	# 		for new_mat in materials:
	# 			if new_mat.name == mat_name:
	# 				print(new_mat.get_nuclides(), material.get_nuclides())

	# Add zero dens nuclide for each material
	def add_zero_dens_nuclides(self, cell):

		material_dict = cell.get_all_materials()
		material = material_dict[list(material_dict.keys())[0]]

		init_nucl = material.get_nuclides()
		cell_name = cell.name
		mat_name = material.name
		# Not sure if this is necessary
		if self.selected_bucells_nucl_list_dict != {}:
			if cell_name in self.selected_bucells_nucl_list_dict:
				nucl_list_input = self.selected_bucells_nucl_list_dict[cell_name]
				if nucl_list_input == 'initial nuclides':
					nucl_list = init_nucl
				elif nucl_list_input == 'NAX':
					NAX_nucl_list_name = utils.zamid_list_to_name_list(data.NAX_nucl_list)
					NAX_nucl_list_name_new_format = utils.bu_namelist_to_mc_namelist(NAX_nucl_list_name)
					# I add init_nucl because I believe it is important for init nuclides to be tallied
					# Their density is usually high enough that their change can influence spectrum etc...
					nucl_list = [x for x in NAX_nucl_list_name_new_format if x in self.MC_XS_nucl_list] + init_nucl
					# Here we remove the potential duplicates
					nucl_list = list(dict.fromkeys(nucl_list))
				else:
					 nucl_list = nucl_list_input
			else:
				nucl_list = self.MC_XS_nucl_list
				#nucl_list = utils.bu_namelist_to_mc_namelist(nucl_list)

		else:
			nucl_list = self.MC_XS_nucl_list

		if not utils.is_lista_in_listb(init_nucl, nucl_list):
			raise Initial_nuclides_not_in_nuclide_list('Some initial nuclides in cell {} material {} are not included in nucl_list'.format(cell_name, mat_name))
		for nucl in nucl_list:
			if nucl not in init_nucl:
				material.add_nuclide(nucl, self.zero_dens_1_atm)

		# Material is rename 'cell name' + 'mat'
		# New material id is 'mat id' + 'cell id'
		material.name = '{} mat'.format(cell_name)
		material.id = int('{}{}'.format(material.id, cell.id))

	@property
	def sequence(self):

		return self._sequence

	@sequence.setter
	def sequence(self, sequence):

		self._sequence = sequence

	def set_sequence(self, sequence):

		self.sequence = sequence
		system = self.system
		system.set_sequence(sequence, mode = 'couple')

	# Add zero dens nuclide for each cell
	# Sort of complicated
	# Will deal with that later
	# def add_zero_dens_nuclides(self, root_cell, nucl_list_dict):

	# 	cell_dict = root_cell.get_all_cells()

	# 	for cell_id in cell_dict:
	# 		cell = cell_dict[cell_id]
	# 		material_dict = cell.get_all_materials()
	# 		material = copy.deepcopy(material_dict[material_dict.key()[0]])
	# 		init_nucl = material.get_nuclides()
	# 		nucl_list = nucl_list_dict[cell_id]
	# 		nucl_list = utils.bu_namelist_to_mc_namelist(nucl_list)
	# 		if not is_lista_in_listb(init_nucl, nucl_list):
	# 			raise Initial_nuclides_not_in_nuclide_list('Some initial nuclides of material {} are not included in nucl_list'.format(mat_id))
	# 		for nucl in nucl_list:
	# 			if nucl not in init_nucl:
	# 				material.add_nuclide(nucl, 1E-22)

	@property
	def init_nucl_dict(self):

		return self._init_nucl_dict

	# Create a dict with initial nuclides of each cell before
	# cell material is added 1 atm nuclides
	def set_init_nucl_dict(self, root_cell):

		cell_dict = root_cell.get_all_cells()

		init_nucl_dict = {}
		for cell_id in cell_dict:
			cell = cell_dict[cell_id]
			material_dict = cell.get_all_materials()
			material = material_dict[list(material_dict.keys())[0]]
			init_nucl = material.get_nuclides()
			init_nucl_dict[cell_id] = init_nucl

		self._init_nucl_dict = init_nucl_dict

		# # If no nucl list has been defined, nucl_list_dict = init_nucl
		# if self.nucl_list_dict == None:
		# 	self.nucl_list_dict = self.init_nucl_dict

	# Use init_nucl_dict and distribute init_nucl to each bucell
	def set_init_nucl(self, cell_dict, bucell_dict):

		init_nucl_dict = self.init_nucl_dict
		for bucell_id in bucell_dict:
			bucell = bucell_dict[bucell_id]
			bucell.init_nucl = init_nucl_dict[bucell_id]

	@property
	def MC_XS_nucl_list(self):

		return self._MC_XS_nucl_list

	@MC_XS_nucl_list.setter
	def MC_XS_nucl_list(self, MC_XS_nucl_list):

		self._MC_XS_nucl_list = MC_XS_nucl_list

	def set_MC_XS_nucl_list(self):

		#path_to_xs_xml = os.environ['OPENMC_CROSS_SECTIONS']
		path_to_xs_xml = self._cross_sections_path + '/cross_sections.xml'

		self.MC_XS_nucl_list = []

		tree = ET.parse(path_to_xs_xml)
		root = tree.getroot()

		for child in root:
			if child.attrib['type'] == 'neutron':
				self._MC_XS_nucl_list.append(child.attrib['materials'])

		# Remove trouble makers	that appears in JEFF32 cross section
		# For some reason, OpenMC can't find these nuclides in jeff lib at 800K
		# self._MC_XS_nucl_list.remove('Cu63')
		# self._MC_XS_nucl_list.remove('Cu65')
		# self._MC_XS_nucl_list.remove('Mn55')
		# # THose are not handled by OpenBU
		# self._MC_XS_nucl_list.remove('C0')
		# self._MC_XS_nucl_list.remove('V0')
		# self._MC_XS_nucl_list.remove('Zn0')

		# Remove trouble makers	that appears in ENDFVIII cross section
		# For some reason, OpenMC can't find these nuclides in jeff lib at 800K
		# self._MC_XS_nucl_list.remove('Cu63')
		# self._MC_XS_nucl_list.remove('Cu65')
		# self._MC_XS_nucl_list.remove('Mn55')
		# # THose are not handled by OpenBU
		try:
			self._MC_XS_nucl_list.remove('C0')
		except ValueError:
			pass
		try:
			self._MC_XS_nucl_list.remove('V0')
		except ValueError:
			pass
		try:
			self._MC_XS_nucl_list.remove('Zn0')
		except ValueError:
			pass

	# When volume is passed from OpenMC to OpenBU
	def pass_vol(self, cell_dict, bucell_dict):

		# Need to loop over bucell_dict because there might be more cells than bucells
		for i in bucell_dict:

			cell = cell_dict[i]
			cell_volume = cell.volume
			bucell = bucell_dict[i]
			bucell.vol = cell_volume

		# root_cell is not in bucell_dict but it contains the info on the total volume
		# Here, the total volume is set to system
		system = self.system
		system.total_vol = self.root_cell.volume

	# When volume is set directly by user
	# Right now this should bet set after import openmc as it overwrites volume calculated by openmc
	def set_vol(self, vol_dict):

		system = self.system
		bucell_dict = system.bucell_dict

		# Need to loop over bucell_dict because there might be more cells than bucells
		for i in bucell_dict:
			bucell = bucell_dict[i]
			if bucell.name in vol_dict:
				bucell.vol = vol_dict[bucell.name]

		# We treat total volume separately
		system.total_vol = vol_dict['total volume']

		self._volume_set = 'yes'

	def pass_nuclide_densities(self, cell_dict, bucell_dict):

		for i in bucell_dict:
			bucell = bucell_dict[i]
			cell = cell_dict[i]
			#init_nucl = self.init_nucl_dict[i]
			init_nucl = bucell.init_nucl
			materials = cell.get_all_materials()
			openmc_dens_dict = materials[list(materials.keys())[0]].get_nuclide_atom_densities()
			openbu_dens_dict = {}

			for nucl in openmc_dens_dict:
				#OpenMC xs has cross section for element carbon and Vanadinium (C0, V0) only. OpenBU can't handle that
				if nucl == 'C0' or nucl == 'V0':
					continue
				openbu_nucl = utils.openmc_name_to_openbu_name(nucl)
				# if nucl is one of the initial non-zero initial nuclide, pass the density
				if nucl in init_nucl:
					openbu_dens_dict[openbu_nucl] = openmc_dens_dict[nucl][1]
				# if nucl is not one of the non-zero initial nuclide, set densiy to zero
				else:
					openbu_dens_dict[openbu_nucl] = 0.0

			bucell = bucell_dict[i]
			bucell.set_initial_dens(openbu_dens_dict)

	def get_bucell_from_cell(self):

		root_cell = self.root_cell
		bucell_dict = {}
		cell_dict = root_cell.get_all_cells()

		for i in cell_dict:
			cell = cell_dict[i]
			cell_name = cell.name
			if cell_name in self.selected_bucells_name_list:
				bucell_dict[i] = Cell(i, cell_name)

		self.set_init_nucl(cell_dict, bucell_dict)
		self.pass_vol(cell_dict, bucell_dict)
		self.pass_nuclide_densities(cell_dict, bucell_dict)

		return bucell_dict

	def get_flux_tally(self, bucell):

		flux = openmc.Tally(name='{} flux'.format(bucell.name))
		flux.filters = [openmc.CellFilter(bucell.id)]
		flux.filters.append(self.energy_bin)
		flux.scores = ['flux']

		return flux

	def get_flux_spectrum_tally(self, bucell):

		flux_spectrum = openmc.Tally(name='{} flux spectrum'.format(bucell.name))
		flux_spectrum.filters = [openmc.CellFilter(bucell.id)]
		flux_spectrum.filters.append(self.mg_energy_bin)
		flux_spectrum.scores = ['flux']

		return flux_spectrum

	# Every nuclide presents in cell material will have its tally taken
	def get_all_nucl_rxn_tally(self, bucell):

		nucl_list = self.get_nucl_to_be_tallied(bucell)
		print ('bucell name',bucell.name)
		print ('nucl list when set to tally',nucl_list)
		nucl_list = utils.bu_namelist_to_mc_namelist(nucl_list)
		rxn = openmc.Tally(name='{} rxn rate'.format(bucell.name))
		rxn.filters = [openmc.CellFilter(bucell.id)]
		rxn.filters.append(self.energy_bin)
		rxn.scores = ['fission', '(n,gamma)', '(n,2n)', '(n,3n)', '(n,p)', '(n,a)']
		#rxn.scores = ['fission', '(n,gamma)']
		#rxn.scores = ['fission', '(n,gamma)', '(n,2n)']
		rxn.nuclides = nucl_list

		return rxn

	def export_material_to_xml(self):

		# Collect materials from each cells and put them into a materials object
		materials = openmc.Materials()
		root_cell = self.root_cell
		cell_dict = root_cell.get_all_cells()
		# When different cells have the same material, the material should not be
		# counted twice
		id_list = []
		for cell_id in cell_dict:
			cell = cell_dict[cell_id]
			material_dict = cell.get_all_materials()
			material = material_dict[list(material_dict.keys())[0]]
			if material.id not in id_list:
				materials.append(material)
				id_list.append(material.id)

		# If the input materials.xml file is in cwd, remove it
		try:
			os.remove(os.getcwd() + '/materials.xml')
		except OSError:
			pass

		# Set the cross section path again to materials
		materials.cross_sections = self._cross_sections_path +'/cross_sections.xml'

		materials.export_to_xml()

	def export_geometry_to_xml(self):

		# Collect each cells and put them into a geometry object
		# Need to re-instantiate a universe that will be filled with the modified root cell
		root_universe = self._root_universe
		geometry = openmc.Geometry(root_universe)

		region = self.root_cell.region
		#print (region.get_surfaces())
		for surface_id in region.get_surfaces():
			surface = region.get_surfaces()[surface_id]
		#quit()

		# If the input materials.xml file is in cwd, remove it
		try:
			os.remove(os.getcwd() + '/geometry.xml')
		except OSError:
			pass

		geometry.export_to_xml()

	def export_tallies_to_xml(self):

		system = self.system

		bucell_dict = system.bucell_dict

		tallies = openmc.Tallies()

		for bucell_id in bucell_dict:
			bucell = bucell_dict[bucell_id]
			flux = self.get_flux_tally(bucell)
			flux_spectrum = self.get_flux_spectrum_tally(bucell)
			rxn = self.get_all_nucl_rxn_tally(bucell)
			tallies.append(flux)
			tallies.append(flux_spectrum)
			tallies.append(rxn)

		# If the input tallies.xml file is in cwd, remove it
		try:
			os.remove(os.getcwd() + '/tallies.xml')
		except OSError:
			pass

		tallies.export_to_xml()

	# settings.xml as provided by the user is read
	# It is then modified and set to couple
	# Since it will not change during the simulation, it is not going
	# to be modified again

	@property
	def settings(self):

		return self._settings

	def _read_user_settings(self):

		system = self.system
		MC_input_path = self.MC_input_path

		file_path = MC_input_path + '/settings.xml'
		tree = ET.parse(file_path)
		root = tree.getroot()
		settings = openmc.Settings()

		for child in root:
			if child.tag == 'particles':
				settings.particles = int(child.text)
				self.partices = int(child.text)
			if child.tag == 'batches':
				settings.batches = int(child.text)
				self.batches = int(child.text)
			if child.tag == 'inactive':
				settings.inactive = int(child.text)
				self.inactive = int(child.text)

		settings.output = {'tallies': False}
		settings.temperature = {'method': 'interpolation'}

		ll = self.bounding_box[0]
		ur = self.bounding_box[1]
		#uniform_dist = openmc.stats.Box(ll, ur, only_fissionable=True)
		point = openmc.stats.Point(xyz=(0.0, 0.0, 0.0))
		settings.source = openmc.source.Source(space=point)
		# To reduce the size of the statepoint file
		settings.sourcepoint['write'] = False

		self._settings = settings

	# the setting file created by the user should be stored somewhere
	def export_settings_to_xml(self):

		settings = self.settings

		# If the input settings.xml file is in cwd, remove it
		try:
			os.remove(os.getcwd() + '/settings.xml')
		except OSError:
			pass

		settings.export_to_xml()

	@property
	def particles(self):

		return self._particles

	@particles.setter
	def particles(self, particles):

		self._particles = particles

	@property
	def batches(self):

		return self._batches

	@batches.setter
	def batches(self, batches):

		self._batches = batches

	@property
	def inactive(self):

		return self._inactive

	@inactive.setter
	def inactive(self, inactive):

		self._inactive = inactive

	@property
	def openmc_bin_path(self):

		return self._openmc_bin_path

	@openmc_bin_path.setter
	def openmc_bin_path(self, openmc_bin_path):

		self._openmc_bin_path = openmc_bin_path

	def run_openmc(self):

		# New material xml file needs to be written with zero dens nuclides
		self.export_material_to_xml()
		# New geometry xml file needs to be written with new materials id
		self.export_geometry_to_xml()
		# Tallies xml files needs to be writen
		self.export_tallies_to_xml()
		# Settings xml files needs to be written
		self.export_settings_to_xml()

		#openmc_bin_path = '/tigress/jdtdl/openmc/py3-mpi-190324/bin/openmc'
		#openpc_bin_path = '/tigress/mkutt/openmc/py3-mpi/bin/openmc'

		if self.MPI == 'on':
			openmc.run(mpi_args=[self._exec, '-n', self._tasks], openmc_exec = self.openmc_bin_path)
		else:
			openmc.run(openmc_exec = self.openmc_bin_path)

		self._set_statepoint()
		self._set_updated_summary()
		# Append the new kinf to system sequence
		self._set_kinf()

	# This method set decay_lib_set to yes
	# It can be used when the user does not want the system to be set any decay data
	def no_decay(self):

		self._decay_lib_set = 'yes'

	def set_decay_lib(self, decay_lib_path):

		system = self.system
		self._decay_lib_set = 'yes'
		self._decay_lib_path = decay_lib_path
		system.set_decay_for_all(decay_lib_path)

	def set_default_decay_lib(self):

		system = self.system
		self._decay_lib_set = 'yes'
		#system.set_default_decay_for_all_no_add()
		self._decay_lib_path = 'default'
		system.set_default_decay_for_all()

	def set_decay_from_object(self, bucell, object):

		system = self.system
		# This should not set yes since it is only for one bucell
		# Need to be fixed later
		self._decay_lib_set = 'yes'
		bucell = system.get_bucell(bucell)
		bucell.set_decay(object)

	def set_xs_lib(self, xs_lib_path):

		system = self.system
		self._xs_lib_set = 'yes'
		system.set_xs_for_all(xs_lib_path)

	def set_default_xs_lib(self):

		system = self.system
		self._xs_lib_set = 'yes'
		#system.set_default_decay_for_all_no_add()
		system.set_default_xs_for_all()

	def set_fy_lib(self, fy_lib_path):

		system = self.system
		self._fy_lib_set = 'yes'
		self._fy_lib_path = fy_lib_path
		system.set_fy_for_all(fy_lib_path)

	def set_default_fy_lib(self):

		system = self.system
		self._fy_lib_set = 'yes'
		self._fy_lib_path = 'default'
		#system.set_default_fy_for_all_no_add()
		system.set_default_fy_for_all()

	def set_fy_from_object(self, bucell, object):

		system = self.system
		# This should not set yes since it is only for one bucell
		# Need to be fixed later
		self._fy_lib_set = 'yes'
		bucell = system.get_bucell(bucell)
		bucell.set_fy(object)

	# This method reads, samples the isomeric branching data and xs data
	# using the mg energy bin mid points data and fold them together
	# def fold_sampled_isomeric_xs_data(self):

	# 	sampled_iso_data = self.get_sampled_isomeric_branching_data()
	# 	sampled_xs_data = self.get_sampled_ng_cross_section_data()

	# 	iso_xs_data = {}
	# 	for nucl in sampled_iso_data:
	# 		iso_data = sampled_iso_data[nucl]
	# 		xs_data = sampled_xs_data[nucl]
	# 		iso_xs_data[nucl] = {}
	# 		iso_xs_data[nucl]['0'] = [x*y for x,y in zip(iso_data['0'], xs_data)]
	# 		iso_xs_data[nucl]['1'] = [x*y for x,y in zip(iso_data['1'], xs_data)]

	# 	self._iso_xs_data = iso_xs_data

	def set_sampled_isomeric_branching_data(self):

		print ('\n\n\n***********Sampling isomeric branching data***********\n\n\n')
		isomeric_branching_data = data.read_isomeric_data()
		sampled_isomeric_branching_data = {}

		for nucl in isomeric_branching_data:
			nucl_data = isomeric_branching_data[nucl]

			# print (nucl_data['0'])
			sampled_isomeric_branching_data[nucl] = {}
			sampled_isomeric_branching_data[nucl]['0'] = nucl_data['0'](self.mg_energy_mid_points)
			sampled_isomeric_branching_data[nucl]['1'] = nucl_data['1'](self.mg_energy_mid_points)

		self._sampled_isomeric_branching_data = sampled_isomeric_branching_data

	# This method reads and samples the point-wise cross section for ng of the nuclides that
	# have ng isomeric branching data only
	def set_sampled_ng_cross_section_data(self):

		print ('\n\n\n***********Sampling point-wise cross section data***********\n\n\n')
		sampled_isomeric_branching_data = self._sampled_isomeric_branching_data
		sampled_ng_cross_section_data = {}
		cross_section_path = self._cross_sections_path
		cross_sections_files_name = os.listdir(cross_section_path)

		total_count = 1
		file_name_list = []
		for file_name in cross_sections_files_name:
			nucl_name = file_name.replace('.h5', '')
			if nucl_name in sampled_isomeric_branching_data:
				total_count += 1
				file_name_list.append(file_name)

		start = time.time()
		count = 1
		for file_name in file_name_list:
			nucl_name = file_name.replace('.h5', '')
			if nucl_name in ['Pm147', 'Am241']: # make it a shorter
				nucl_path = cross_section_path+'/{}'.format(file_name)
				xs_data = openmc.data.IncidentNeutron.from_hdf5(nucl_path)
				ng_xs_data = xs_data[102].xs['294K']
				print ('--- Sampling {} (n,gamma) point-wise cross section --- [{}/{}]'.format(nucl_name, count, total_count))
				sampled_ng_xs_data = ng_xs_data(self.mg_energy_mid_points)
				sampled_ng_cross_section_data[nucl_name] = sampled_ng_xs_data
				count += 1
		end = time.time()
		print('\n Time to sample cross sections: {}'.format(end - start))

		self._sampled_ng_cross_section_data = sampled_ng_cross_section_data

	def set_tallies_to_bucells(self, s):

		system = self.system
		bucell_dict = system.bucell_dict
		sp = self.statepoint
		summary = self.updated_summary
		xs_mode  = self.xs_mode
		sampled_isomeric_branching_data = self._sampled_isomeric_branching_data
		sampled_ng_cross_section_data = self._sampled_ng_cross_section_data

		for bucell_id in bucell_dict:
			bucell = bucell_dict[bucell_id]

			# densities from OpenMC are extracted
			# This is for nuclides which are zero in OpenBU but set to 1E-24 in OpenMC
			# 1E-24 fraction percent does not translate into 1E-24 atm/cm3 always
			# Therefore, the code needs to divide the reaction rate by the correct densities
			# taken from summary.geometry.cell.material
			cell = summary.geometry.get_cells_by_name(bucell.name)[0]
			material_dict = cell.get_all_materials()
			material = material_dict[list(material_dict.keys())[0]]
			mc_nuclides_densities = material.get_nuclide_atom_densities()

			flux_tally = sp.get_tally(name = '{} flux'.format(bucell.name))
			flux_spectrum_tally = sp.get_tally(name = '{} flux spectrum'.format(bucell.name))
			rxn_rate_tally = sp.get_tally(name = '{} rxn rate'.format(bucell.name))
			bucell._set_MC_tallies(mc_nuclides_densities, flux_tally, flux_spectrum_tally, rxn_rate_tally, sampled_isomeric_branching_data, sampled_ng_cross_section_data, xs_mode, s)

		# YOU NEED TO CREATE LIST TO STORE EACH NEW XS

	# Normalize the flux in each cell with the FMF after each openmc calculation
	# Calculate the power of each cell from the normalized flux
	def step_normalization(self, s):

		system = self.system
		sequence = self.sequence
		FMF = sequence.get_FMF1(system, s)

		bucell_list = system.get_bucell_list()
		for bucell in bucell_list:
			bucell_sequence = bucell.sequence
			MC_flux = bucell_sequence.current_MC_flux
			# MC_flux is volume integrated (unit cm.sp
			# FMF is in sp.s
			# to have flux in cm.s you need to divide by volule of the cell
			flux = FMF*MC_flux/bucell.vol
			pow_dens = bucell._update_pow_dens(flux)
			print ('initial', pow_dens)
			bucell_sequence._set_macrostep_flux(flux)
			bucell_sequence._set_macrostep_pow_dens(pow_dens)

	# Normalize the flux in each cell with the FMF after each openmc calculation
	# Calculate the power of each cell from the normalized flux
	def initial_couple_step_normalization(self, norma_mode):

		system = self.system
		sequence = self.sequence
		FMF = sequence.get_FMF1(system, 0)

		bucell_list = system.get_bucell_list()
		for bucell in bucell_list:
			bucell_sequence = bucell.sequence
			MC_flux = bucell_sequence.current_MC_flux
			flux = FMF*MC_flux
			pow_dens = bucell._update_pow_dens(flux)
			bucell_sequence._set_initial_flux(flux)
			bucell_sequence._set_initial_pow_dens(pow_dens)

	def copy_MC_files(self, s):

		step_folder = '/step_{}'.format(s)
		openmc_file_path = os.getcwd()+step_folder+'/OpenMC'

		os.mkdir(openmc_file_path)

		all_MC_files = glob.glob('*.xml') + glob.glob('tallies.out') + glob.glob('*.h5')
		for file_name in all_MC_files:
			try:
				shutil.copyfile(os.getcwd()+'/{}'.format(file_name), openmc_file_path + '/{}'.format(file_name))
			except IOError:
				continue

		for file_name in all_MC_files:
			os.remove(os.getcwd() + '/{}'.format(file_name))

	def set_dens_to_cells(self):

		summary = self.initial_summary
		system = self.system
		bucell_dict = system.bucell_dict
		for bucell_id in bucell_dict:
			bucell = bucell_dict[bucell_id]
			tally_nucl_list = utils.mc_namelist_to_bu_namelist(self.get_nucl_to_be_tallied(bucell))
			cell = summary.geometry.get_cells_by_name(bucell.name)[0]
			material_dict = cell.get_all_materials()
			material = material_dict[list(material_dict.keys())[0]]
			### WARNING Openmc add_nuclide in atom percent is not in absolute atom percent
			### but in relative ao, i.e., setting U235 and U238 to 0.34 and 0.34 will be the same
			### as setting them to 50% and 50 %
			### Therefore the ao value that needs to be updated needs to be normalize by the tot dens of
			### only the nuclides to be tallied and not by the total dens of all nuclide in bucell
			tally_nucl_list_dens = bucell.get_subtotal_dens_counting_zero_dens(tally_nucl_list)
			material.set_density('atom/b-cm', tally_nucl_list_dens)
			for nucl in tally_nucl_list:
				openmc_nucl_name = utils.openbu_name_to_openmc_name(nucl)
				material.remove_nuclide(openmc_nucl_name)
				# No need to calculate ao, just directly give density
				#nucl_subao = bucell.get_nucl_subao(nucl, tally_nucl_list)
				nucl_dens = bucell.get_nucl_dens_for_openmc(nucl)
				material.add_nuclide(openmc_nucl_name, nucl_dens)

	def set_MC_XS_nuc_list_to_bucells(self):

		system = self.system
		bucell_dict = system.bucell_dict
		MC_XS_nucl_list = self.MC_XS_nucl_list
		MC_XS_nucl_list_obu_name =  utils.mc_namelist_to_bu_namelist(MC_XS_nucl_list)
		MC_XS_nucl_list_zamid = utils.name_list_to_zamid_list(MC_XS_nucl_list_obu_name)
		for bucell_id in bucell_dict:
			bucell = bucell_dict[bucell_id]
			bucell.MC_XS_nucl_list = MC_XS_nucl_list_zamid

	def burn(self):

		start_time = time.time()

		# If no decay libs and fy libs have been set, set default libs
		if self._decay_lib_set == 'no':
			self.set_default_decay_lib()
			print ('\n\n\n----  Default decay constants library set for system  ----\n---- {} ----'.format(data.default_decay_b_lib_path))
		else:
			print ('\n\n\n----  User defined path for decay library  ----\n\n')
			print ('----  {}  ----\n\n\n'.format(self._decay_lib_path))

		if self._fy_lib_set == 'no':
			self.set_default_fy_lib()
			print ('\n\n\n----  Default fission yields library set for system  ----\n---- {} ----'.format(data.default_fy_lib_path))
		else:
			print ('\n\n\n----  User defined path for fission yields library ----\n\n')
			print ('----  {}  ----\n\n\n'.format(self._fy_lib_path))

		#print (self.xs_mode, self._xs_lib_set)
		if self.xs_mode == 'constant lib' and self._xs_lib_set == 'no':
			self.set_default_xs_lib()
			print ('\n\n\n----Default cross section library set for system----\n\n\n')
		else:
			# This method simply pass the MC_XS_nucl_list to each cell so that each cell
			# can then build it own lib_nucl_list
			self.set_MC_XS_nuc_list_to_bucells()

			print ('\n\n\n----  Path for cross sections library ----\n\n')
			print ('----  {}  ----\n\n\n'.format(self._cross_sections_path))

		self.set_sampled_isomeric_branching_data()
		self.set_sampled_ng_cross_section_data()

		system = self.system
		sequence = system.sequence
		norma_mode = sequence.norma_unit

		# Check consistency of nuclides list &
		# Generate leaves for each cell
		# I attempted to update set_all_leaves in order to be able to
		# sort out nuclides and reduce nuclide set
		# But it is too complicated and risk to break the code
		bucell_list = system.get_bucell_list()
		for bucell in bucell_list:
			# Check if different nuclide list (initial list, lib list and nucl set (user defined set of nuclides to be considered))
			# are consistent with each other
			bucell._check_nucl_list_consistency()
			# Create a list of all nuclides that should be produced, i.e., that belong to the network tree
			#bucell._reduce_nucl_set()

		# This should be somewhere else but for now it is done here
		system.zam_order_passlist()

		#steps_number = sequence.steps_number
		steps_number = sequence.macrosteps_number
		# Shift loop from 1 in order to align loop s and step indexes
		for s in range(1, steps_number+1):

			print ('\n\n\n\n====== STEP {}======\n\n\n\n'.format(s))
			sequence.gen_step_folder(s)
			print (('\n\n\n=== OpenMC Transport {}===\n\n\n'.format(s)))
			self.run_openmc()
			self.set_tallies_to_bucells(s)
			self.step_normalization(s)
			self.copy_MC_files(s)
			print (('\n\n\n=== Salameche Burn {}===\n\n\n'.format(s)))
			salameche.burn_step(system, s, 'couple')
			self.set_dens_to_cells()

		# This last openmc_run is used to compute the last burnup/time point kinf
		print ('\n\n\n=== OpenMC Transport for Final Point===\n\n\n')
		self.run_openmc()

		system._gen_output_summary_folder()
		system._print_summary_allreacs_rank()
		system._print_summary_subdens()
		system._print_summary_dens()
		system._print_summary_xs()
		system._print_summary_flux_spectrum(self.mg_energy)
		system._print_summary_kinf()
		system._print_summary_param()
		system._print_summary_isomeric_branching_ratio()

		run_time = time.time() - start_time
		print ('\n\n\n >>>>>> OpenBU burn took {} seconds <<<<<<< \n\n\n'.format(run_time))

class Initial_nuclides_not_in_nuclide_list(Exception):
	"""Raise when some initial nuclides are not included in nucl_list """
	pass

class STOP(Exception):
	"""Just a way to stop the code"""
	pass


#!/usr/bin/env python
# coding: utf-8

# #keyence in single field zstack multicolor capture mode
# #yields an overlay and 4 single channel tif files (CH1,CH2,CH3,CH4 AS BGRW)
# #for each z position
# #example:
# #B-DAPI G-PCAD R-RFP W-A6_20X (good exposure)_Z001_CH1.tif
# #B-DAPI G-PCAD R-RFP W-A6_20X (good exposure)_Z001_CH2.tif
# #B-DAPI G-PCAD R-RFP W-A6_20X (good exposure)_Z001_CH3.tif
# #B-DAPI G-PCAD R-RFP W-A6_20X (good exposure)_Z001_CH4.tif
# #B-DAPI G-PCAD R-RFP W-A6_20X (good exposure)_Z001_Overlay.tif
#
# #problem is the 4th color is white which contaminates other channels
# #only psuedocolors used should be RGB
# #this script should take a folder with these images and
# # (1) make 3 color overlays (excluding one color)
# # (2) stack the 3 color overlays and save as tif

import os
import re
import numpy as np
import pprint
from multiprocessing import Process, Pool, freeze_support
import cv2
import time
import zipfile
import time
import tifffile
import imagecodecs
import sys
import argparse

#FIRST PIECE OF THE PUZZLE i think the whole thing is correct
def calcContrast(image, w_x, w_y): #for max contrast projection
    image = image.astype('float')
    kernel_fill = 1/((2*w_x+2)*(2*w_y+2))
    kernel_row = np.array([kernel_fill for x in range(0,2*w_y+2)]) #this is definitely correct
    kernel = np.array([kernel_row for x in range(0,2*w_x+2)]) #this is definitely correct

    img_squared = np.square(image.astype('float'))
    #print('img_squared: %s' % img_squared.dtype)

    contrast = cv2.filter2D(img_squared, -1, kernel) - np.square(cv2.filter2D(image, -1, kernel)) #neg value means result array same data depth as source

    return contrast

def getContrastStack(imageStack, w_x, w_y): #for max contrast projection
    # Calculate the contrast for each image in the stack
    temp_stack = []
    #contrast_stack = lapply(img_frames, calcContrast, w_x, w_y, brushShape)
    for frame in imageStack:
        temp=calcContrast(frame, w_x, w_y)
        temp_stack.append(temp) #i just needed to combine the 3 layers somehow...
    #contrast_stack = np.dstack(temp_stack)
    contrast_stack = temp_stack
    return contrast_stack #single channel MCP

def blurContrastStack(contraststack,pixel_offset=1,cstacks_blur_kernel=(9,9),blur_sd=5): #for max contrast projection (single channel)
    ########comment out if shit breaks
    f_cstacks = []
    for each_cstack in contraststack:
        max_mask = np.array(list(map(lambda x: x < np.max(x)*pixel_offset,each_cstack)))
        blurred = cv2.GaussianBlur(max_mask*each_cstack,cstacks_blur_kernel,sigmaX=blur_sd)
        f_cstacks.append(blurred)
    return f_cstacks

def contrastProjection(input_image_stack,var_kernel_row=1,
                       var_kernel_col=1,cstacks_blur_kernel=(9,9),pixel_offset=1,blur_sd=5): #for max contrast projection THIS IS THE MAIN RUN FUNCTION TO GET SINGLE CHANNEL MCPs

    #var_kernel_row = 1 #1 was good
    #var_kernel_col = 1 #1 was good
    #cstacks_blur_kernel = (9,9) #was using 3,3 before with ok results. 7,7 seems to blend away the halo a bit
    #pixel_offset = 1 #don't need an offset anymore since figuring out the float problem
    #blur_sd = 5 #3was good so was 1, maybe 1 best, went to 5 with a 5,5 blur kernel and saw no diff.

    #calculate contrast for each single color image "calcContrast"
    cstack = getContrastStack(input_image_stack, var_kernel_row, var_kernel_col)
    blurred_cstack = blurContrastStack(cstack,pixel_offset,cstacks_blur_kernel,blur_sd)
    blurred_cstack = np.array(blurred_cstack) #turn contrast stacks into an array and sort along z at each element for highest layer index
    max_mapped = np.argmax(blurred_cstack,axis=0) #single layer in shape of single input image with elements representing best z layer index

    proj = np.full(max_mapped.shape, 0)
    for row in range(max_mapped.shape[0]):
        for col in range(max_mapped.shape[1]):
            proj[row,col] = input_image_stack[max_mapped[row,col]][row,col]
    #print(proj.dtype,proj.max())
    proj=np.uint8(proj)

    return proj #final max contrast projection image (SINGLE CHANNEL)

def projectImage(worklist,projection='intensity'): #Maximum Intensity Projection (use projection='contrast' for max contrast projections)
    r_layers = []
    g_layers = []
    b_layers = []
    w_layers = []
    for item in worklist:
        r = tifffile.imread(item[0])
        g = tifffile.imread(item[1])
        b = tifffile.imread(item[2])
        w = tifffile.imread(item[3])
        try:
            if np.average(r[:,:,0])>0: #if R multilayered since keyence saves even single chann as RGB 3 layer
                r = r[:,:,0]
            elif np.average(r[:,:,1])>0:
                r = r[:,:,1]
            elif np.average(r[:,:,2])>0:
                r = r[:,:,2]
            else:
                r = r[:,:,2] #in case all channels are pure black
        except:
            r = r[:,:]

        try:
            if np.average(g[:,:,0])>0: #if G multilayered
                g = g[:,:,0]
            elif np.average(g[:,:,1])>0:
                g = g[:,:,1]
            elif np.average(g[:,:,2])>0:
                g = g[:,:,2]
            else:
                g = g[:,:,1]
        except:
            g = g[:,:]

        try:
            if np.average(b[:,:,0])>0: #if B multilayered
                b = b[:,:,0]
            elif np.average(b[:,:,1])>0:
                b = b[:,:,1]
            elif np.average(b[:,:,2])>0:
                b = b[:,:,2]
            else:
                b = b[:,:,0]
        except:
            b = b[:,:]

        try:
            if np.average(w[:,:,0])>0: #if W multilayered
                w = w[:,:,0]
            elif np.average(w[:,:,1])>0:
                w = w[:,:,1]
            elif np.average(w[:,:,2])>0:
                w = w[:,:,2]
            else:
                w = w[:,:,0]
        except:
            w = w[:,:]

        r_layers.append(r)
        g_layers.append(g)
        b_layers.append(b)
        w_layers.append(w)

        if projection == 'intensity':
            r_proj = np.max(r_layers,axis=0) #max intensity projection
            g_proj = np.max(g_layers,axis=0) #max intensity projection
            b_proj = np.max(b_layers,axis=0) #max intensity projection
            w_proj = np.max(w_layers,axis=0) #max intensity projection
        if projection == 'contrast':  #calculate contrast for each single color image "calcContrast"
            r_proj = contrastProjection(r_layers) #max contrast projection
            g_proj = contrastProjection(g_layers) #max contrast projection
            b_proj = contrastProjection(b_layers) #max contrast projection
            w_proj = contrastProjection(w_layers) #max contrast projection
        return [r_proj,g_proj,b_proj,w_proj]

def allocator(worklist): #chunks the worklist for a given tile into blocks of 30 four color zlevels
    blocklist = []
    processing_blocks = []
    if len(worklist) < 30:
        blocklist.append((0,len(worklist)))

    else:
        block = len(worklist)
        for block in range(block//30):
            start = block*30
            end = start+30
            blocklist.append((start,end))
    if blocklist[-1][1] != len(worklist):
        blocklist.append((blocklist[-1][1],len(worklist))) #append the remainder block if there is one
    pp.pprint('processing block... # blocks per tile: ' + str(len(blocklist))+' (30 images max per block)')

    for zrange in blocklist:
    #pp.pprint(worklist[zrange[0]:zrange[1]])
        processing_blocks.append(worklist[zrange[0]:zrange[1]])

    return list(zip(blocklist,processing_blocks))

def blocksize(worklist): #for progress calculation displays
    blocklist = []
    if len(worklist) < 30:
        blocklist.append((0,len(worklist)))
    else:
        block = len(worklist)
        for block in range(block//30):
            start = block*30
            end = start+30
            blocklist.append((start,end))
    if blocklist[-1][1] != len(worklist):
        blocklist.append((blocklist[-1][1],len(worklist))) #append the remainder block if there is one
    return int(len(blocklist))

def getworklist(tile):
    worklist = []
    try:
        for z in unique_z_pos:
            r = proc_folder+base_name+tile+'_'+z+'_'+channels['R']+'.tif'
            g = proc_folder+base_name+tile+'_'+z+'_'+channels['G']+'.tif'
            b = proc_folder+base_name+tile+'_'+z+'_'+channels['B']+'.tif'
            w = proc_folder+base_name+tile+'_'+z+'_'+channels['W']+'.tif'
            worklist.append([r,g,b,w])
        return worklist
    except:
        print("oh god it isn't a stitched image")
        for z in unique_z_pos:
            r = proc_folder+base_name+z+'_'+channels['R']+'.tif'
            g = proc_folder+base_name+z+'_'+channels['G']+'.tif'
            b = proc_folder+base_name+z+'_'+channels['B']+'.tif'
            w = proc_folder+base_name+z+'_'+channels['W']+'.tif'
            worklist.append([r,g,b,w])
        return worklist

def col_fusion(img_list,rows,cols,overlap):
    print('**********\nstarting column fusion\n**********')
    print('grid dimensions are %d rows by %d columns\n' %  (rows,cols))

    column_fusions = []
    if cols > 1:

        ims = sorted(img_list)
        mylist = [x for x in range(0,len(img_list))]
        advance_one = mylist[1:]

        all_rows = mylist[::cols]
        left_right_rows = mylist[::cols*2]
        right_left_rows = sorted(list(set(all_rows)-set(left_right_rows)))

        print('column fusion: L-R rows are %s' % str(left_right_rows))
        print('column fusion: R-L rows are %s' % str(right_left_rows))

        ###fuse left to right rows overlapping inside tile edges###
        for row in left_right_rows:
            foo1 = []
            i=0
            middle_col_count = 0
            for tile in [x for x in range(0,cols)]:
                if i < cols:
                    fusion_img = tifffile.imread(ims[row+i])
                    if len(fusion_img.shape) > 2:
                        col_dims = fusion_img.shape[1]
                        col_margin = int(col_dims * overlap)
                        col_margin_alpha = int(col_margin/1)
                        #print(margin)
                        if i == 0: #left edge tile
                            fusion_img[:,-col_margin_alpha:col_dims,:] = fusion_img[:,-col_margin_alpha:col_dims,:]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo1.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: L-R left')
                        elif i == cols-1: #right edge tile
                            fusion_img[:,0:col_margin_alpha,:] = fusion_img[:,0:col_margin_alpha,:]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo1.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: L-R right')
                        else:
                            fusion_img[:,-col_margin_alpha:col_dims,:] = fusion_img[:,-col_margin_alpha:col_dims,:]*0.5 #ALPHA BLENDING CODE ADDITION
                            fusion_img[:,0:col_margin_alpha,:] = fusion_img[:,0:col_margin_alpha,:]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo1.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            middle_col_count += 1 #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: L-R mid')
                    else:
                        col_dims = fusion_img.shape[1]
                        col_margin = int(col_dims * overlap)
                        col_margin_alpha = int(col_margin/1)
                        #print(margin)
                        if i == 0: #left edge tile
                            fusion_img[:,-col_margin_alpha:col_dims] = fusion_img[:,-col_margin_alpha:col_dims]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo1.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: L-R left')
                        elif i == cols-1: #right edge tile
                            fusion_img[:,0:col_margin_alpha] = fusion_img[:,0:col_margin_alpha]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo1.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: L-R right')
                        else:
                            fusion_img[:,-col_margin_alpha:col_dims] = fusion_img[:,-col_margin_alpha:col_dims]*0.5 #ALPHA BLENDING CODE ADDITION
                            fusion_img[:,0:col_margin_alpha] = fusion_img[:,0:col_margin_alpha]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo1.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            middle_col_count += 1 #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: L-R mid')
            print('running column fusion: L-R rows')
            print([x.shape for x in foo1])
            #foobar1 = cv2.hconcat([x for x in foo1])

            #column_fusions.append((row,foobar1))
            if len(fusion_img.shape)>2:
                #print('RGB (fusion_img.shape)>2)')

                dst_arry_width = int((fusion_img.shape[1]*len(foo1))-(fusion_img.shape[1]*(middle_col_count+1)*overlap)) #ALPHA BLENDING CODE ADDITION
                #dst_arry_width_p1 = int((fusion_img.shape[1]*len(foo1))-(fusion_img.shape[1]*(middle_col_count+1)*overlap))+1 #ALPHA BLENDING CODE ADDITION
                #dst_arry_width_m1 = int((fusion_img.shape[1]*len(foo1))-(fusion_img.shape[1]*(middle_col_count+1)*overlap))-1 #ALPHA BLENDING CODE ADDITION
                #if dst_arry_width-fusion_img.shape[1]==0:
                #    pass
                #elif dst_arry_width-fusion_img.shape[1]<0:
                #   dst_arry_width = dst_arry_width_p1 #account for rounding errors1
                #else:
                #    dst_arry_width = dst_arry_width_m1 #account for rounding errors

                dst_arry_col_position = 0 #ALPHA BLENDING CODE ADDITION
                dst_arry = np.full((fusion_img.shape[0],dst_arry_width,3),0) #ALPHA BLENDING CODE ADDITION
                #print('dst_arry.shape %s' % str(dst_arry.shape))

                for col in foo1: #ALPHA BLENDING CODE ADDITION
                    #print(dst_arry_col_position+int(col.shape[1]*overlap))
                    dst_arry[:,dst_arry_col_position:dst_arry_col_position+col.shape[1],:] =  dst_arry[:,dst_arry_col_position:dst_arry_col_position+col.shape[1],:]+col[:,:,:] #ALPHA BLENDING CODE ADDITION
                    dst_arry_col_position += int(col.shape[1]-col_margin) #ALPHA BLENDING CODE ADDITION

                column_fusions.append((row,dst_arry))

            else:
                #print('WHITE (fusion_img.shape)<2)')

                dst_arry_width = int((fusion_img.shape[1]*len(foo1))-(fusion_img.shape[1]*(middle_col_count+1)*overlap)) #ALPHA BLENDING CODE ADDITION
                dst_arry_width_p1 = int((fusion_img.shape[1]*len(foo1))-(fusion_img.shape[1]*(middle_col_count+1)*overlap))+1 #ALPHA BLENDING CODE ADDITION
                dst_arry_width_m1 = int((fusion_img.shape[1]*len(foo1))-(fusion_img.shape[1]*(middle_col_count+1)*overlap))-1 #ALPHA BLENDING CODE ADDITION
                #if dst_arry_width-fusion_img.shape[1]==0:
                #    pass
                #elif dst_arry_width-fusion_img.shape[1]<0:
                #   dst_arry_width = dst_arry_width_p1 #account for rounding errors1
                #else:
                #    dst_arry_width = dst_arry_width_m1 #account for rounding errors
                dst_arry_col_position = 0 #ALPHA BLENDING CODE ADDITION
                dst_arry = np.full((fusion_img.shape[0],dst_arry_width),0) #ALPHA BLENDING CODE ADDITION
                #print('dst_arry.shape %s' % str(dst_arry.shape))

                for col in foo1: #ALPHA BLENDING CODE ADDITION
                    #print(dst_arry_col_position+int(col.shape[1]*overlap))
                    dst_arry[:,dst_arry_col_position:dst_arry_col_position+col.shape[1]] =  dst_arry[:,dst_arry_col_position:dst_arry_col_position+col.shape[1]]+col[:,:] #ALPHA BLENDING CODE ADDITION
                    dst_arry_col_position += int(col.shape[1]-col_margin) #ALPHA BLENDING CODE ADDITION

                column_fusions.append((row,dst_arry))

            ###fuse right to left rows###
        for row in right_left_rows:
            foo2 = []
            middle_col_count = 0
            i=0
            for tile in [x for x in range(0,cols)]:
                if i < cols:
                    fusion_img = tifffile.imread(ims[row+i])
                    if len(fusion_img.shape) > 2:
                        col_dims = fusion_img.shape[1]
                        col_margin = int(col_dims * overlap)
                        col_margin_alpha = int(col_margin/1)
                        #print(margin)
                        if i == 0: #RIGHT edge tile (OPPOSITE FROM ABOVE)
                            fusion_img[:,0:col_margin_alpha,:] = fusion_img[:,0:col_margin_alpha,:]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo2.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: R-L right')
                        elif i == cols-1: #LEFT edge tile
                            fusion_img[:,-col_margin_alpha:col_dims,:] = fusion_img[:,-col_margin_alpha:col_dims,:]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo2.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: R-L left')
                        else:
                            fusion_img[:,-col_margin_alpha:col_dims,:] = fusion_img[:,-col_margin_alpha:col_dims,:]*0.5 #ALPHA BLENDING CODE ADDITION
                            fusion_img[:,0:col_margin_alpha,:] = fusion_img[:,0:col_margin_alpha,:]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo2.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            middle_col_count += 1 #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: R-L mid')
                    else:
                        col_dims = fusion_img.shape[1]
                        col_margin = int(col_dims * overlap)
                        col_margin_alpha = int(col_margin/1)
                        #print(margin)
                        if i == 0: #RIGHT edge tile (OPPOSITE FROM ABOVE)
                            fusion_img[:,0:col_margin_alpha] = fusion_img[:,0:col_margin_alpha]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo2.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: R-L right')
                        elif i == cols-1: #LEFT edge tile
                            fusion_img[:,-col_margin_alpha:col_dims] = fusion_img[:,-col_margin_alpha:col_dims]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo2.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: R-L left')
                        else:
                            fusion_img[:,-col_margin_alpha:col_dims] = fusion_img[:,-col_margin_alpha:col_dims]*0.5 #ALPHA BLENDING CODE ADDITION
                            fusion_img[:,0:col_margin_alpha] = fusion_img[:,0:col_margin_alpha]*0.5 #ALPHA BLENDING CODE ADDITION
                            foo2.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                            middle_col_count += 1 #ALPHA BLENDING CODE ADDITION
                            i+=1
                            print('column fusion: R-L mid')
            print('running column fusion: R-L rows')
            print([x.shape for x in foo2])
            #foobar2 = cv2.hconcat([x for x in foo2][::-1]) #reverse the list so it fuses right since the tiles snaked back

            #column_fusions.append((row,foobar2))
            if len(fusion_img.shape)>2:
                #print('RGB (fusion_img.shape)>2)')

                dst_arry_width = int((fusion_img.shape[1]*len(foo2))-(fusion_img.shape[1]*(middle_col_count+1)*overlap)) #ALPHA BLENDING CODE ADDITION
                dst_arry_width_p1 = int((fusion_img.shape[1]*len(foo2))-(fusion_img.shape[1]*(middle_col_count+1)*overlap))+1 #ALPHA BLENDING CODE ADDITION
                dst_arry_width_m1 = int((fusion_img.shape[1]*len(foo2))-(fusion_img.shape[1]*(middle_col_count+1)*overlap))-1 #ALPHA BLENDING CODE ADDITION
                #if dst_arry_width-fusion_img.shape[1]==0:
                #    pass
                #elif dst_arry_width-fusion_img.shape[1]<0:
                #   dst_arry_width = dst_arry_width_p1 #account for rounding errors1
                #else:
                #    dst_arry_width = dst_arry_width_m1 #account for rounding errors

                dst_arry_col_position = 0 #ALPHA BLENDING CODE ADDITION
                dst_arry = np.full((fusion_img.shape[0],dst_arry_width,3),0) #ALPHA BLENDING CODE ADDITION
                #print('dst_arry.shape %s' % str(dst_arry.shape))

                for col in foo2[::-1]: #ALPHA BLENDING CODE ADDITION
                    #print(dst_arry_col_position+int(col.shape[1]*overlap))
                    dst_arry[:,dst_arry_col_position:dst_arry_col_position+col.shape[1],:] =  dst_arry[:,dst_arry_col_position:dst_arry_col_position+col.shape[1],:]+col[:,:,:] #ALPHA BLENDING CODE ADDITION
                    dst_arry_col_position += int(col.shape[1]-col_margin) #ALPHA BLENDING CODE ADDITION

                column_fusions.append((row,dst_arry))

            else:
                #print('WHITE (fusion_img.shape)<2)')
                dst_arry_width = int((fusion_img.shape[1]*len(foo2))-(fusion_img.shape[1]*(middle_col_count+1)*overlap)) #ALPHA BLENDING CODE ADDITION
                dst_arry_width_p1 = int((fusion_img.shape[1]*len(foo2))-(fusion_img.shape[1]*(middle_col_count+1)*overlap))+1 #ALPHA BLENDING CODE ADDITION
                dst_arry_width_m1 = int((fusion_img.shape[1]*len(foo2))-(fusion_img.shape[1]*(middle_col_count+1)*overlap))-1 #ALPHA BLENDING CODE ADDITION
                #if dst_arry_width-fusion_img.shape[1]==0:
                #    pass
                #elif dst_arry_width-fusion_img.shape[1]<0:
                #   dst_arry_width = dst_arry_width_p1 #account for rounding errors1
                #else:
                #    dst_arry_width = dst_arry_width_m1 #account for rounding errors

                dst_arry_col_position = 0 #ALPHA BLENDING CODE ADDITION
                dst_arry = np.full((fusion_img.shape[0],dst_arry_width),0) #ALPHA BLENDING CODE ADDITION
                #print('dst_arry.shape %s' % str(dst_arry.shape))

                for col in foo2[::-1]: #ALPHA BLENDING CODE ADDITION
                    #print(dst_arry_col_position+int(col.shape[1]*overlap))
                    dst_arry[:,dst_arry_col_position:dst_arry_col_position+col.shape[1]] =  dst_arry[:,dst_arry_col_position:dst_arry_col_position+col.shape[1]]+col[:,:] #ALPHA BLENDING CODE ADDITION
                    dst_arry_col_position += int(col.shape[1]-col_margin) #ALPHA BLENDING CODE ADDITION

                column_fusions.append((row,dst_arry))

            column_fusions = sorted(column_fusions)

            print('column fusion has %d/%d items' % (len(column_fusions),rows))
    else:
        #return the images from the single column
        ims = sorted(img_list)
        i = 0
        for image in ims:
            image = tifffile.imread(image)
            column_fusions.append((i,image))
            i+=1
        print("FIX")
    return column_fusions

def row_fusion(fused_cols,rows,cols,overlap):
    print('\n**********\nstarting row fusion\n**********')
    print('grid dimensions are %d rows by %d columns\n' %  (rows,cols))
    row_fusions = []
    column_fusions=sorted(fused_cols)
    middle_row_count = 0 #ALPHA BLENDING CODE ADDITION
    if rows > 1:
        j=0
        for cf in [x for x in range(0,rows)]:
            print('cf is %d' % cf)
            fusion_img = column_fusions[cf][1]
            #print('fusion img shape is ' +str(fusion_img.shape))
            row_dims = fusion_img.shape[0]
            row_margin = int(row_dims * overlap)
            row_margin_alpha = int(row_margin/1)

            #print(margin)
            if j == 0: #TOP FUSED ROW
                fusion_img[-row_margin_alpha:row_dims,:] = fusion_img[-row_margin_alpha:row_dims,:]*0.5 #ALPHA BLENDING CODE ADDITION
                row_fusions.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                #row_fusions.append(fusion_img[0:-row_margin,:]) #ALPHA BLENDING CODE DELETION
                j+=1
                print('row fusion: top')
            elif j == rows-1: #BOTTOM FUSED ROW
                fusion_img[0:row_margin_alpha,:] = fusion_img[0:row_margin_alpha,:]*0.5 #ALPHA BLENDING CODE ADDITION
                row_fusions.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                #row_fusions.append(fusion_img[row_margin:row_dims,:]) #ALPHA BLENDING CODE DELETION
                j+=1
                print('row fusion: bottom')
            else:
                fusion_img[-row_margin_alpha:row_dims,:] = fusion_img[-row_margin_alpha:row_dims,:]*0.5 #ALPHA BLENDING CODE ADDITION
                fusion_img[0:row_margin_alpha,:] = fusion_img[0:row_margin_alpha,:]*0.5 #ALPHA BLENDING CODE ADDITION
                row_fusions.append(fusion_img) #ALPHA BLENDING CODE ADDITION
                #row_fusions.append(fusion_img[row_margin:-row_margin,:]) #MIDDLE FUSED ROW(S) #ALPHA BLENDING CODE DELETION
                j+=1
                middle_row_count += 1
                print('row fusion: mid')

        print('row fusion has %d/%d items' % (len(row_fusions),rows))
        print('running row fusion')
        dst_arry_height = int((fusion_img.shape[0]*len(row_fusions))-(fusion_img.shape[0]*(middle_row_count+1)*overlap)) #ALPHA BLENDING CODE ADDITION
        dst_arry_height_p1 = int((fusion_img.shape[0]*len(row_fusions))-(fusion_img.shape[0]*(middle_row_count+1)*overlap))+1 #ALPHA BLENDING CODE ADDITION
        dst_arry_height_m1 = int((fusion_img.shape[0]*len(row_fusions))-(fusion_img.shape[0]*(middle_row_count+1)*overlap))-1 #ALPHA BLENDING CODE ADDITION
        if dst_arry_height-fusion_img.shape[0] == 0:
            pass
        elif dst_arry_height-fusion_img.shape[1] < 0:
            dst_arry_height = dst_arry_height_p1 #account for rounding errors1
        else:
            dst_arry_height = dst_arry_height_m1 #account for rounding errors

        print('fusion_img.shape is %s' % str(fusion_img.shape))

        if len(fusion_img.shape)>2:
            print('RGB (fusion_img.shape)>2)')

            dst_arry_height = int((fusion_img.shape[0]*len(row_fusions))-(fusion_img.shape[0]*(middle_row_count+1)*overlap)) #ALPHA BLENDING CODE ADDITION
            dst_arry_row_position = 0 #ALPHA BLENDING CODE ADDITION
            dst_arry = np.full((dst_arry_height,fusion_img.shape[1],3),0) #ALPHA BLENDING CODE ADDITION
            print('dst_arry.shape %s' % str(dst_arry.shape))

            for row in row_fusions: #ALPHA BLENDING CODE ADDITION
                print(dst_arry_row_position+int(row.shape[0]*overlap))
                dst_arry[dst_arry_row_position:dst_arry_row_position+row.shape[0],:,:] =  dst_arry[dst_arry_row_position:dst_arry_row_position+row.shape[0],:,:]+row[:,:,:] #ALPHA BLENDING CODE ADDITION
                dst_arry_row_position += int(row.shape[0]-row_margin) #ALPHA BLENDING CODE ADDITION

        else:
            print('WHITE (fusion_img.shape)<2)')

            dst_arry_height = int((fusion_img.shape[0]*len(row_fusions))-(fusion_img.shape[0]*(middle_row_count+1)*overlap)) #ALPHA BLENDING CODE ADDITION
            dst_arry_row_position = 0 #ALPHA BLENDING CODE ADDITION
            dst_arry = np.full((dst_arry_height,fusion_img.shape[1]),0) #ALPHA BLENDING CODE ADDITION
            print('dst_arry.shape %s' % str(dst_arry.shape))

            for row in row_fusions: #ALPHA BLENDING CODE ADDITION
                print(dst_arry_row_position+int(row.shape[0]*overlap))
                dst_arry[dst_arry_row_position:dst_arry_row_position+row.shape[0],:] =  dst_arry[dst_arry_row_position:dst_arry_row_position+row.shape[0],:]+row[:,:] #ALPHA BLENDING CODE ADDITION
                dst_arry_row_position += int(row.shape[0]-row_margin) #ALPHA BLENDING CODE ADDITION

        final_fusion = dst_arry #ALPHA BLENDING CODE ADDITION
        #final_fusion = cv2.vconcat(row_fusions) #fuse the rows #ALPHA BLENDING CODE DELETION
        return final_fusion
        #final_fusion = cv2.vconcat(row_fusions) #fuse the rows

    else:
        final_fusion = column_fusions[0][1] #WHAT IF THERE IS ONLY 1 ROW AND NOTHING TO FUSE?
    return final_fusion

#find all the tile positions. keyence tiles in a snaking pattern left to right then down and right to left, etc and the overlap is ~30% in both x and y
#1 2 3
#6 5 4
#7 8 9
def find_tiles(fins):
    try:
        tile_re = re.compile(r'\d\d\d\d\d')
        tiles = []
        for file in fins:
            tiles.append(re.search(tile_re,file).group(0))
        tiles = np.unique(tiles).tolist()
        return tiles
    except:
        print("oh god it isn't a stitched image")
        return None

def main(tiles,unique_zs,projection_mode='intensity'):
    #putting it all together- the run script

    if tiles != None:
        k = 1 #part of blocks progress counter
        for tile in tiles:
            worklist = [] #list of tiles, and tiles contain all z stacks of all 4 color channels
            worklist.append(getworklist(tile))
            worklist = worklist[0] #de-nestifying
            current_block = k * 4 * len(np.unique(unique_zs))
            print('tile %s: worklist completion is %d/%d items' % (tile,current_block,len(worklist)))
            k += 1
            #process the blocks with multiprocessing (pool and map)
            with Pool() as po:
                res = po.map_async(projectImage, ((i,projection_mode) for i in allocator(worklist)[0][1]))
                fetch = res.get()

            r_layers = []
            g_layers = []
            b_layers = []
            w_layers = []
            for chunked_intermediate in fetch:
                r = chunked_intermediate[0]
                g = chunked_intermediate[1]
                b = chunked_intermediate[2]
                w = chunked_intermediate[3]
                r_layers.append(r)
                g_layers.append(g)
                b_layers.append(b)
                w_layers.append(w)
            if projection_mode == 'intensity':
                projectImage_r = np.max(r_layers,axis=0)
                projectImage_g = np.max(g_layers,axis=0)
                projectImage_b = np.max(b_layers,axis=0)
                projectImage_w = np.max(w_layers,axis=0)

            if projection_mode == 'contrast':
                projectImage_r = contrastProjection(r_layers)
                projectImage_g = contrastProjection(g_layers)
                projectImage_b = contrastProjection(b_layers)
                projectImage_w = contrastProjection(w_layers)

            rgb = np.dstack([projectImage_r,projectImage_g,projectImage_b])

            tifffile.imwrite(proc_folder+timestamp+'_FF_'+base_name+tile+'_RGB.tif',rgb.astype('uint8')) #can't stitch multicolor images
            #tifffile.imwrite(proc_folder+timestamp+'_FF_'+base_name+tile+'_R.tif',projectImage_r)
            #tifffile.imwrite(proc_folder+timestamp+'_FF_'+base_name+tile+'_G.tif',projectImage_g)
            #tifffile.imwrite(proc_folder+timestamp+'_FF_'+base_name+tile+'_B.tif',projectImage_b)
            tifffile.imwrite(proc_folder+timestamp+'_FF_'+base_name+tile+'_W.tif',projectImage_w.astype('uint8'))
        print("tiled image job done")

    else:
        print("going to single image workflow")

        worklist = getworklist(tifs_no_overlays)
            #worklist.append(getworklist(tifs_no_overlays))
            #worklist = worklist[0] #de-nestifying

            #process the blocks with multiprocessing (pool and map)
        with Pool() as po:
            res = po.map_async(projectImage, ((i,projection_mode) for i in allocator(worklist)[0][1]))
            fetch = res.get()

        r_layers = []
        g_layers = []
        b_layers = []
        w_layers = []
        for chunked_intermediate in fetch:
            r = chunked_intermediate[0]
            g = chunked_intermediate[1]
            b = chunked_intermediate[2]
            w = chunked_intermediate[3]
            r_layers.append(r)
            g_layers.append(g)
            b_layers.append(b)
            w_layers.append(w)
        projectImage_r = np.max(r_layers,axis=0)
        projectImage_g = np.max(g_layers,axis=0)
        projectImage_b = np.max(b_layers,axis=0)
        projectImage_w = np.max(w_layers,axis=0)

        rgb = np.dstack([projectImage_r,projectImage_g,projectImage_b])

        tifffile.imwrite(proc_folder+timestamp+'_FF_'+base_name+'_RGB.tif',rgb.astype('uint8')) #can't stitch multicolor images
            #io.imsave(proc_folder+'FF_'+base_name+tile+'_R.tif',projectImage_r)
            #io.imsave(proc_folder+'FF_'+base_name+tile+'_G.tif',projectImage_g)
            #io.imsave(proc_folder+'FF_'+base_name+tile+'_B.tif',projectImage_b)
        tifffile.imwrite(proc_folder+timestamp+'_FF_'+base_name+'_W.tif',projectImage_w.astype('uint8'))

        print("single stack job done")

if __name__ == "__main__":

    freeze_support()
    timestamp = time.strftime("%Y%m%d-%H%M%S")
    start_time = time.time()

    print('@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@')
    print('Written by Wesley Wong @ Hsu Lab, Harvard, Department of Stem Cell & Regenerative Biology 1/18/2021\n\
           after getting very frustrated with all the clicking and waiting from the actual software.')
    print('\nThis script takes a folder of images and accompanying .bcf file by the BZ-700 microscope made by Keyence\nand returns a\
        Full Focused (FF) by Maximum Intensity Projection image, stitching together all the tiles\n if thereare tiles present')
    print('\n The tiled images produced by this script are simple edge to edge concatenations without any\nplanar corrections.\
        This tool should be used to quickly stitch and assess images for more precise processing if worthwhile.\n\
            The output FF images can be used by Photoshop, FIJI, etc to stitch better final fusions.')
    print('@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@\n')

    parser = argparse.ArgumentParser()
    parser.add_argument("path", help="path of the folder with single channel .tif and .bcf file output from Keyence BZ-X700", type=str)
    parser.add_argument("--contrast", help="project z-stack by maximum contrast pixels (default mode is by maximum intensity pixels",action='store_true')
        accounting for planar distortion (default is simple joining along seams",action='store_true')
    args = parser.parse_args()

    if args.contrast:
        print('Producing maximum contrast projections!')
        projection_mode = 'contrast'
    else:
        print('Defaulting to maximum INTENSITY projections!')
        projection_mode = 'intensity'
    target_path = args.path #temporary placeholder to avoid null value exception
    if len(sys.argv) > 1:
        target_path = args.path
    else:
        print('ERROR! SEE BELOW AND PLEASE PAY ATTENTION TO THE EXACT SYNTAX INCLUDING SLASHES AND QUOTATION MARKS (SINGLE \nAND DOUBLE ONES)\n')
        sys.exit("Usage: bzimage <path> \n\nEXAMPLE: bzimage '/Users/wesleywong/single-field-zstack copy merge/early jlv bot/'\n\n\
                 the path should point to a folder of micrographs made by BZ-Analyzer\n\
                 containing TIFs of CH1-CH4 (there must be all 4 channels) and the .bcf file\n\
                 the script is now exiting")
    #proc_folder = "/Users/wesleywong/single-field-zstack copy merge/axial/" #this one is a 8x3 ear stitch made with a custom merge area. WORKS.
    #proc_folder = "/Users/wesleywong/single-field-zstack copy merge/P33 CD1/" #this one is a 3x3 stitch made with custom merge area. WORKS.
    #proc_folder = "/Users/wesleywong/single-field-zstack copy merge/early jlv bot/" #20x single image from multipoint. WORKS.
    #proc_folder = "/Users/wesleywong/single-field-zstack copy merge/20X_CD451_CC3_3X5_STD/" #5x3 center-offset image. WORKS.

    proc_folder = target_path

    #remember the camera for the keyence captures the individual color channels as:
    # B (CH1),
    # G (CH2),
    # R (CH3),
    # W (CH4)

    #pseudocolor assignments
    channels = {
                'R':'CH3',
                'G':'CH2',
                'B':'CH1',
                'W':'CH4'
                }

    #get all the single channel tifs
    only_tifs = re.compile(r'.*CH.[.]tif')

    tifs_no_overlays = []
    for file in os.listdir(proc_folder):
        if re.match(only_tifs,file):
            tifs_no_overlays.append(file)

    pp = pprint.PrettyPrinter()
    print('sample file names of the images to be stitched:')
    pp.pprint(tifs_no_overlays[0:5])
    tiles = find_tiles(tifs_no_overlays)

    print('unique tiles found for stitching:')
    pp.pprint(tiles)

    #find all the z-positions
    zpos = re.compile(r'Z\d\d\d')
    unique_z_pos = []
    for file in tifs_no_overlays:
        unique_z_pos.append(re.search(zpos,file).group(0))
    #print('later calculated length of unique z: %d' % len(unique_z_pos))

    #get image family base name
    if tiles != None: #if there are actually tiles to stitch
        image_family_name = re.compile(r'^.*\d\d\d\d\d_Z')
        base_name = re.search(image_family_name, tifs_no_overlays[0]).group(0)[:-7] #without tile or z pos
        print('image series root name: %s' %base_name)
    else:
        image_family_name = re.compile(r'^.*_Z')
        base_name = re.search(image_family_name, tifs_no_overlays[0]).group(0)[:-1] #without tile or z pos
        print('image series root name: %s' %base_name)

    #call main function which includes the minimal multiprocessing component
    main(tiles,unique_z_pos,projection_mode)

    #continue processing from the multiprocess stacked images
    if tiles !=None:
        #get all the single channel tifs
        RE_RGB = re.compile(timestamp+r'.*_FF_.*\d\d\d\d\d_RGB.tif')
        RE_W = re.compile(timestamp+r'.*_FF_.*\d\d\d\d\d_W.tif')

        finished_projs_rgb = []
        for file in os.listdir(proc_folder):
            if re.match(RE_RGB,file):
                finished_projs_rgb.append(proc_folder+file)
        finished_projs_rgb = sorted(finished_projs_rgb)

        finished_projs_w = []
        for file in os.listdir(proc_folder):
            if re.match(RE_W,file):
                finished_projs_w.append(proc_folder+file)
        finished_projs_w = sorted(finished_projs_w)

        print('sample of finished max intensity projections for each tile position:')
        pp.pprint(finished_projs_rgb[0:2])
        pp.pprint(finished_projs_w[0:2])
    else:
        pass

    if tiles != None:
        #extract information from .bcf file

        with zipfile.ZipFile(proc_folder+base_name[:-1]+'.bcf') as myzip:
            #pp.pprint(myzip.infolist())
            lens_info = myzip.read('GroupFileProperty/Lens/properties.xml')
            lens_info = re.search('<LensName.*</LensName>',str(lens_info)).group(0)
            lens_info = re.search('\s\d*x\s',lens_info).group(0)
            lens_info = lens_info[1:-1]
            row_dims = myzip.read('GroupFileProperty/RangeSelection/ImageJointRow/properties.xml')
            row_dims = re.search('\d*</LimitUpper>',str(row_dims)).group(0)
            row_dims = int(row_dims[:-13])+1 #need more sophisticated number finding since it's sometimes double digit
            col_dims = myzip.read('GroupFileProperty/RangeSelection/ImageJointColumn/properties.xml')
            col_dims = re.search('\d*</LimitUpper>',str(col_dims)).group(0)
            col_dims = int(col_dims[:-13])+1 #need more sophisticated number finding since it's sometimes double digit
            print('microscope objective: %s' % lens_info)
            print('imaging tile rows: %s' % row_dims)
            print('imaging tile cols: %s' % col_dims)
    else:
        pass

    if tiles != None:
        overlap_percent = 0.30 #+0.005+0.0025 #0.3 works but the stitching scars are noticeable #1-17-21, WW: 0.3075 works best
        fuse_cols_rgb = col_fusion(finished_projs_rgb,row_dims,col_dims,overlap_percent)
        fuse_rows_rgb = row_fusion(fuse_cols_rgb,row_dims,col_dims,overlap_percent) #0.31 for row fusion might be better than .3 both
        tifffile.imwrite(proc_folder+timestamp+'_'+str(lens_info)+'_'+str(row_dims)+'X'+str(col_dims)+'_final_fusion_RGB.tif',fuse_rows_rgb.astype('uint8'))

        fuse_cols_w = col_fusion(finished_projs_w,row_dims,col_dims,overlap_percent)
        fuse_rows_w = row_fusion(fuse_cols_w,row_dims,col_dims,overlap_percent)
        tifffile.imwrite(proc_folder+timestamp+'_'+str(lens_info)+'_'+str(row_dims)+'X'+str(col_dims)+'_final_fusion_W.tif',fuse_rows_w.astype('uint8'))

    else:
        pass

    stop_time = time.time()
    print('took %d seconds' % (stop_time-start_time))


import os
import sys
# Remove Keras Debug Info
stdout = sys.stdout
sys.stdout =  open(os.devnull, 'w')
stderr = sys.stderr
sys.stderr = open(os.devnull, 'w')
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'

import cv2
import numpy as np
import skimage
from skimage.transform import resize
from keras.models import load_model
from skimage import data, io
from matplotlib import pyplot as plt

image_size = 128

class FaceExtractor():

    faceCascade = None

    def __init__(self, cascadePath):
        self.faceCascade = cv2.CascadeClassifier(cascadePath)

    def getFace(self, imagePath):
        image = cv2.imread(imagePath)
        gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)

        faces = self.faceCascade.detectMultiScale(
            gray,
            scaleFactor=1.1,
            minNeighbors=5,
            minSize=(100, 100)
        )
        optFace = None

        if len(faces) < 1:
            optFace = image

        elif len(faces) >= 1:
            (x, y, w, h) = faces[0]
            for (x1, y1, w1, h1) in faces:
                a_opt = w * h
                a_face = w1 * h1
                if a_opt < a_face:
                    (x, y, w, h) = (x1, y1, w1, h1)

            optFace = image[y:y+h, x:x+w]

        retImage = cv2.resize(optFace, (image_size, image_size))
        return retImage

def PrintResults(results):
    values = results[0]
    lable_classes = ['Angry', 'Contemptuous', 'Disgusted', 'Fearful', 'Happy', 'Neutral', 'Sad', 'Surprised']

    width = 28
    print('-'*width)
    print('|' + ' '*5 + 'Class'  + ' '*5+ '| Score')
    print('-'*width)
    for i in values.argsort()[::-1]:
        print(f'| {lable_classes[i]}' + ' '* (14-len(lable_classes[i])) + f'| {values[i]*100:>.2f}%')
        print('-'*width)

def main():
    sys.stdout = stdout
    sys.stderr = stderr
    faceCrop = FaceExtractor('./utils/haarcascade_frontalface_default.xml')

    print('Loading Model...')
    model = load_model('./models/alexnet.h5')
    X =np.array([faceCrop.getFace(sys.argv[1]),])
    results =  model.predict(X)
    PrintResults(results)

main()