# This file provides the interface to all compressors

from subprocess import call, check_output
import os
import datetime
import shutil
from math import log2, inf
from functools import reduce
from copy import deepcopy

import datasets
from util import *

# Constants
BASE_COMPRESSED_DECOMPRESSED_PATH = "../tmp/"
BASE_COMPRESSED_SUFFIX = "compressed"
BASE_DECOMPRESSED_SUFFIX = "decompressed"
COMPRESSED_PATH = BASE_COMPRESSED_DECOMPRESSED_PATH + BASE_COMPRESSED_SUFFIX
DECOMPRESSED_PATH = BASE_COMPRESSED_DECOMPRESSED_PATH + BASE_DECOMPRESSED_SUFFIX
COMPRESS = 0
DECOMPRESS = 1
INF_MAX_MODES = 100 # Just a high upper bound, we don't ever consider such high-dimensional datasets

def make_temp_paths_unique(suffix, parallel_file_system=False):
	global COMPRESSED_PATH, DECOMPRESSED_PATH
	base_path = VSC_LOCAL_SCRATCH if (on_vsc() and not parallel_file_system) else BASE_COMPRESSED_DECOMPRESSED_PATH
	COMPRESSED_PATH = base_path + BASE_COMPRESSED_SUFFIX + "-" + suffix.replace(" ", "_")
	DECOMPRESSED_PATH = base_path + BASE_DECOMPRESSED_SUFFIX + "-" + suffix.replace(" ", "_")

def get_compressed_path():
	return COMPRESSED_PATH

def get_decompressed_path():
	return DECOMPRESSED_PATH

# Removes compressed and decompressed files
# COMPRESSED_PATH and DECOMPRESSED_PATH may also be directories, in the case of compressors which store their output in multiple files (e.g. TuckerMPI)
def cleanup():
	for path in (COMPRESSED_PATH, DECOMPRESSED_PATH, COMPRESSED_PATH + "-metadata", DECOMPRESSED_PATH + "-metadata"):
		if os.path.isfile(path):
			os.remove(path)
		elif os.path.isdir(path):
			shutil.rmtree(path)

# Get Python3 command on this system
def get_python3_command():
	res = check_output(["which", "python3"])
	if res == "":
		# python3 is not recognized on this system, so python is probably the correct command
		return "python"
	else:
		# python3 is recognized on this system
		return "python3"
PYTHON3 = get_python3_command()

class Compressor:
	# name: string
	# generate_command: function with signature (mode, dataset, target_error=None, threads=1, environment_variables=dict()) => command (list of strings)
	# estimate_target_error: function with signature (target_psnr, data_range=None, dataset=None, prev_target_error=None, prev_psnr=None) => target_error which estimates target error using this compressor's error metric in terms of a desired PSNR
	# get_valid_target_errors: function with signature (dataset) => list of target_errors which is used for compressors such as FPZIP with discrete target errors, in these cases it is usually hard to map target PSNR's to target errors
	# get_valid_target_errors returns valid target errors in order of decreasing actual error
	def __init__(self, name, max_modes, supported_dtypes, generate_command, estimate_target_error, get_valid_target_errors=None, needs_parallel_io=False):
		self.name = name
		self.max_modes = max_modes
		self.supported_dtypes = supported_dtypes
		self.generate_command = generate_command
		self.estimate_target_error = estimate_target_error
		self.get_valid_target_errors = get_valid_target_errors
		self.needs_parallel_io = needs_parallel_io

# LIST OF COMPRESSORS AND THEIR INTERFACES

compressors = []

# The following estimator is useful for compressors which use a target PSNR value but might produce a somewhat lower/higher error
def standard_correcting_psnr_estimate_target_psnr(target_psnr, data_range=None, dataset=None, prev_target_error=None, prev_psnr=None):
	if prev_target_error is None:
		return target_psnr
	else:
		# Correct based on previous results, assuming offset remains constant
		return target_psnr + (prev_target_error - prev_psnr)

def standard_correcting_abs_bound_estimate_target_error(target_psnr, data_range=None, dataset=None, prev_target_error=None, prev_psnr=None):
	if data_range is None:
		raise Exception("Cannot estimate initial target absolute error bound without data range!")
	if prev_target_error is None:
		# No previous compression, make a "blind" guess
		# We estimate by assuming that RMSE ~= absolute error tolerance
		offset = 35 # Empirical estimate
		return data_range/10**((target_psnr - offset)/20)
	else:
		# Just extrapolate based on the previous compression result
		return prev_target_error/10**((target_psnr - prev_psnr)/20)

# SZ

def sz_generate_command(mode, dataset, target_error=None, threads=1):
	
	command = ["../compressors/sz",]
	
	# Files
	command.extend([
		"-i", dataset.path,
		"-z", COMPRESSED_PATH
	] if mode == COMPRESS else [
		"-s", COMPRESSED_PATH,
		"-x", DECOMPRESSED_PATH
	])
	
	# Data type
	command.append("-f" if dataset.dtype == "float32" else "-d")
	# Mode sizes in least-significant-index first order
	command.extend(["-%s"%dataset.modes,] + list(map(str, dataset.sizes[::-1])))
	
	# Compression information
	if mode == COMPRESS:
		command.extend([
			"-M", "PSNR",
			"-S", str(target_error) # Error metric: PSNR, early support in SZ 3.1.3
		])
	
	if threads > 1:
		command.extend(["-c", "../compressors/sz.config"])
	
	# Tucker decomposition is only supported for double-precision floating point data
	# However, when turning this option on no compressed file is created at the target location, only uncompressed files are created in a newly created "compressed" folder
	# In any case, we don't want this
	#if dataset.dtype == "float64":
	#	command.append("-T")
	# Set environment variable to set path to TuckerMPI
	#environment_variables["TUCKERMPI_HOME"] = "../TuckerMPI/build"
	
	sz_env = os.environ.copy()
	sz_env["OMP_NUM_THREADS"] = str(threads)
	
	return command, sz_env

# SZ has native PSNR support

# SZ supports 5 modes internally, but for some reason only 4 through the command line ...
# And perhaps also deeper? Just modifiying sz.c is not sufficient to eliminate the restriction
SZ = Compressor("SZ", 4, ["float32", "float64"], sz_generate_command, standard_correcting_psnr_estimate_target_psnr)
compressors.append(SZ)

# ZFP

def zfp_generate_command(mode, dataset, target_error=None, threads=1):
	
	command = ["../compressors/zfp", "-h"]
	
	# Files
	command.extend([
		"-i", dataset.path,
		"-z", COMPRESSED_PATH
	] if mode == COMPRESS else [
		"-z", COMPRESSED_PATH,
		"-o", DECOMPRESSED_PATH
	])
	
	# Compression information
	if mode == COMPRESS:
		
		# Data type
		command.extend(["-t", {
			"int32": "i32",
			"int64": "i64",
			"float32": "f32",
			"float64": "f64"
		}[dataset.dtype]])
		
		# Mode sizes in least-significant-index first order
		command.extend(["-%s"%dataset.modes,] + list(map(str, dataset.sizes[::-1])))
		
		# Error metric: absolute error tolerance
		# According to https://computing.llnl.gov/projects/zfp/zfp-compression-ratio-and-quality this yields the best compression outcomes by a large margin
		command.extend(["-a", str(target_error)])
	
	# ZFP decompression simply reports failure when trying to use multithreading, so avoid it in this case
	if threads > 1 and mode == COMPRESS:
		command.extend(["-x", "omp=%s"%threads])
	
	return command

ZFP = Compressor("ZFP", 4, ["int32", "int64", "float32", "float64"], zfp_generate_command, standard_correcting_abs_bound_estimate_target_error)
compressors.append(ZFP)

# FPZIP

def fpzip_generate_command(mode, dataset, target_error=None, threads=1):
	
	command = ["../compressors/fpzip"]
	
	# Files
	command.extend([
		"-i", dataset.path,
		"-o", COMPRESSED_PATH
	] if mode == COMPRESS else [
		"-d",
		"-i", COMPRESSED_PATH,
		"-o", DECOMPRESSED_PATH
	])
	
	# Compression information
	if mode == COMPRESS:
		
		# Data type
		command.extend(["-t", {
			"float32": "float",
			"float64": "double"
		}[dataset.dtype]])
		
		# Mode sizes in least-significant-index first order
		command.extend(["-%s"%dataset.modes,] + list(map(str, dataset.sizes[::-1])))
		
		# Error metric: precision
		command.extend(["-p", str(target_error)])
	
	return command

def fpzip_estimate_target_error(target_psnr, data_range=None, dataset=None, prev_target_error=None, prev_psnr=None):
	
	# target error 0 means lossless compression
		
	if prev_target_error is None or prev_target_error == "0":
		# No previous compression, make a "blind" guess
		# We estimate based by assuming that the RMSE ~= datamax*2^-precision ~= (data_range/2)*2^-precision
		offset = -35 # Empirical estimate
		result = (target_psnr - offset)/(20*log10(2)) - 1
	else:
		# Just extrapolate based on the previous compression result
		# We assume that adding one bit of precision halves the error
		result = prev_target_error + log2(10)*((target_psnr - prev_psnr)/20)
	
	# Precision must be an even value and in the range 2-32 (float32) or 4-64 (float64)
	# If desired precision is too high, just perform lossless compression
	min_prec, max_prec = (2, 32) if dataset.dtype == "float32" else (4, 64)
	if result >= max_prec + 1:
		return 0 # Full precision, lossless compression
	if result < min_prec - 1:
		return min_prec
	result = round(result/2)*2 # Round to the nearest even value
	return min(max_prec, max(min_prec, result)) # Clamp precision

def fpzip_get_valid_target_errors(dataset):
	return list(range(2, (32 if dataset.dtype == "float32" else 64) + 1, 2)) + [0,]

FPZIP = Compressor("FPZIP", 4, ["float32", "float64"], fpzip_generate_command, fpzip_estimate_target_error, get_valid_target_errors=fpzip_get_valid_target_errors)
compressors.append(FPZIP)

# TuckerMPI

def write_tuckermpi_params_file(path, params):
	with open(path, "w") as f:
		for key in params.keys():
			f.write("%s = %s\n"%(key, params[key]))

def list_to_tuckermpi_str(data):
	return " ".join(map(str, data))

def generate_tuckermpi_compressor(single_threaded=False, shared_memory_flags=True):
	
	def tuckermpi_generate_command(mode, dataset, target_error=None, threads=1):
		
		# TuckerMPI expects data of type scalar_t, which is set at compile-time for each executable using a macro variable
		# Should be double for the executables we're using
		
		if single_threaded:
			threads = 1
		
		tuckermpi_formatted_sizes = dataset.sizes[::-1]
		grid_dims = [1]*dataset.modes
		grid_dims[np.argmax(tuckermpi_formatted_sizes)] = threads
		grid_dims_str = list_to_tuckermpi_str(grid_dims)
		tuckermpi_env = os.environ.copy()
		if not on_vsc():
			for var_name, suffix in (("PATH", "bin"), ("LD_LIBRARY_PATH", "lib")):
				tuckermpi_env[var_name] = os.getcwd() + "/../openmpi-1.10.7/install/" + suffix + ":" + tuckermpi_env[var_name]
		tuckermpi_env["OMP_NUM_THREADS"] = "1" # Very important in order to get decent performance! Else each OpenMPI process may spawn e.g. 36 threads ...
		
		if mode == COMPRESS:
			
			metadata_path = COMPRESSED_PATH + "-metadata"
			input_file_list_file = metadata_path + "/raw.txt"
			os.mkdir(COMPRESSED_PATH)
			os.mkdir(metadata_path)
			with open(input_file_list_file, "w") as f:
				f.write(dataset.path)
			
			parameter_file = metadata_path + "/params.txt"
			write_tuckermpi_params_file(parameter_file, {
				"Global dims": list_to_tuckermpi_str(tuckermpi_formatted_sizes),
				"Grid dims": grid_dims_str,
				"Automatic rank determination": "true",
				"SV Threshold": target_error,
				"Input file list": input_file_list_file,
				"Scaling type": "None",
				"Perform STHOSVD": "true",
				"Stats file": metadata_path + "/stats.txt",
				"Write core tensor and factor matrices": "true", # Documentation uses the parameter name "Write STHOSVD result" here, but this does not match the code
				"STHOSVD directory": COMPRESSED_PATH,
				"SV directory": metadata_path,
				"Timing file": metadata_path + "/runtime.csv"
			})
			
			base_command = ["../compressors/tuckermpi-compress-%s"%dataset.dtype, "--parameter-file", parameter_file]
			
		elif mode == DECOMPRESS:
			
			metadata_path = DECOMPRESSED_PATH + "-metadata"
			output_file_list_file = metadata_path + "/rec.txt"
			os.mkdir(metadata_path)
			with open(output_file_list_file, "w") as f:
				f.write(DECOMPRESSED_PATH)
			
			parameter_file = metadata_path + "/params.txt"
			write_tuckermpi_params_file(parameter_file, {
				"Global dims": list_to_tuckermpi_str(tuckermpi_formatted_sizes),
				"Grid dims": grid_dims_str,
				"Beginning subscripts": list_to_tuckermpi_str([0]*dataset.modes),
				"Ending subscripts": list_to_tuckermpi_str([size - 1 for size in tuckermpi_formatted_sizes]),
				"STHOSVD directory": COMPRESSED_PATH,
				"Output file list": output_file_list_file,
			})
			
			base_command = ["../compressors/tuckermpi-decompress-%s"%dataset.dtype, "--parameter-file", parameter_file]
		
		mpi_wrapper_command = []
		if not single_threaded:
			mpi_wrapper_command.append("mpirun")
			if shared_memory_flags:
				mpi_wrapper_command.extend(["--mca", "btl", "self,sm,tcp"])
			mpi_wrapper_command.extend(["-n", str(threads)])
		return (mpi_wrapper_command + base_command), tuckermpi_env

	def tuckermpi_estimate_target_error(target_psnr, data_range=None, dataset=None, prev_target_error=None, prev_psnr=None):
		
		# Error needs to be strictly smaller than 1, so 1 itself can't be used as an upper bound
		# 0.5 should already be too high of an error for all practical purposes
		if prev_target_error is None:
			return min(0.5, psnr_to_relative_error(target_psnr, data_range, dataset.size(), dataset.get_data_norm()))
		else:
			# E.g. if PSNR was 20 dB too high, multiply target relative error by 10
			return min(0.5, prev_target_error*10**((prev_psnr - target_psnr)/20))
	
	parenthesized_name_info = []
	if single_threaded:
		parenthesized_name_info.append("1 process")
	if not shared_memory_flags:
		parenthesized_name_info.append("no SMF")
	name = "TuckerMPI"
	if len(parenthesized_name_info) > 0:
		name += " (%s)"%(", ".join(parenthesized_name_info))
	
	"""
	Concerning parallel IO, a few tests using Isotropic-PT on a login node of the VSC (3 runs per case, timings in seconds):
	I/O to parallel file system:
	Compression: 6.3-6.5
	Decompression: 4.3-4.4
	I/O to local file system ($TMPDIR):
	6.0-6.1
	3.2-3.7
	So the parallel file system doesn't seem to be necessary after all, just like on our laptop we can also use a local physical disk.
	However, the non-local non-parallel file system, such as $VSC_DATA, led to poor performance in the past.
	"""
	
	return Compressor(name, INF_MAX_MODES, ["float32", "float64"], tuckermpi_generate_command, tuckermpi_estimate_target_error, needs_parallel_io=False)

TuckerMPI = generate_tuckermpi_compressor(False)
compressors.append(TuckerMPI)

# TTHRESH

def tthresh_generate_command(mode, dataset, target_error=None, threads=1):
	
	command = ["../compressors/tthresh", "-v"] # Verbose so we can check the log to see the state of progress
	
	# Files
	command.extend([
		"-i", dataset.path,
		"-c", COMPRESSED_PATH
	] if mode == COMPRESS else [
		"-c", COMPRESSED_PATH,
		"-o", DECOMPRESSED_PATH
	])
	
	# Compression information
	if mode == COMPRESS:
		
		# Data type
		command.extend(["-t", {
			"uint8": "uchar",
			"uint16": "ushort",
			"int32": "int",
			"float32": "float",
			"float64": "double"
		}[dataset.dtype]])
		
		# Mode sizes in least-significant-index first order
		command.append("-s")
		command.extend(list(map(str, dataset.sizes[::-1])))
		
		# Error metric: target PSNR
		command.extend(["-p", str(target_error)])
	
	tthresh_env = os.environ.copy()
	tthresh_env["OMP_NUM_THREADS"] = str(threads) # We can't explicitly control the number of threads using CLI arguments, so we do it with an environment variable
	
	return command, tthresh_env

# TTHRESH uses a different PSNR definition, so the same PSNR here will correspond to a 2x lower error
def tthresh_correcting_psnr_estimate_target_psnr(target_psnr, data_range=None, dataset=None, prev_target_error=None, prev_psnr=None):
	res = standard_correcting_psnr_estimate_target_psnr(target_psnr, data_range=data_range, dataset=dataset, prev_target_error=prev_target_error, prev_psnr=prev_psnr)
	if prev_target_error is None:
		res -= 20*log10(2)
	return res

TTHRESH = Compressor("TTHRESH", INF_MAX_MODES, ["uint8", "uint16", "int32", "float32", "float64"], tthresh_generate_command, tthresh_correcting_psnr_estimate_target_psnr)
compressors.append(TTHRESH)

# ATC

ATC_DISABLE_OPTIMIZATION_CLI_ARGS_BASE = [
	["--no-rank-truncation"],
	["--qb", "1"],
	["--ignore-dequantization-correction"],
	["--no-householder"],
	["--mode-storage-order"],
	["--no-split-bit-plane-truncation"]
]
ATC_MAX_OPTIMIZATION_LEVEL = len(ATC_DISABLE_OPTIMIZATION_CLI_ARGS_BASE)

def generate_atc_compressor(rtmss=None, dtmss=None, epcm=False, blas=True, lpmt=None, qrem=None, hwm=None, optimization_level=ATC_MAX_OPTIMIZATION_LEVEL, run=0):
	
	# optimization_level = 0: All optimizations disabled, so all of the aforementioned CLI arguments will be applied:
	
	# Define command generation function using RTMSS parameter
	def atc_generate_command(mode, dataset, target_error=None, threads=1):
		
		command = ["../compressors/atc" + ("-blas" if blas else ""), "--verbose", "--print-results"] # Extra options so we can check the log to see the state of progress and also timings for further analysis
		
		# Files
		command.extend([
			"--of", dataset.path,
			"--cf", COMPRESSED_PATH
		] if mode == COMPRESS else [
			"--cf", COMPRESSED_PATH,
			"--df", DECOMPRESSED_PATH
		])
		
		# Compression information
		if mode == COMPRESS:
			
			# Data type
			command.extend(["--type", dataset.dtype])
			
			# Mode sizes in least-significant-index first order
			command.extend(["--storage-order", "msi", "--sizes"] + list(map(str, dataset.sizes)))
			
			# Error metric: target PSNR
			command.extend(["--psnr", str(target_error)])
			
			# Control rank truncation
			if rtmss is not None:
				command.extend(["--rank-truncation", str(rtmss)])
			
			# Control DCT truncation
			if dtmss is not None:
				command.extend(["--dct", "--dct-truncation", str(dtmss)])
			
			if epcm:
				command.extend(["--epcm"])
			
			if lpmt is not None:
				command.extend(["--lpmt", str(lpmt)])
			
			if qrem is not None:
				command.extend(["--qrem", str(qrem)])
			
			if hwm is not None:
				command.extend(["--hwm", hwm])
			
			disable_optimization_cli_args = deepcopy(ATC_DISABLE_OPTIMIZATION_CLI_ARGS_BASE)
			disable_optimization_cli_args[disable_optimization_cli_args.index(["--mode-storage-order"])].extend(map(str, range(dataset.modes)))
			for level in range(optimization_level, ATC_MAX_OPTIMIZATION_LEVEL):
				command.extend(disable_optimization_cli_args[level])
		
		# Control number of threads
		command.extend(["--threads", str(threads)])
		
		return command
	
	parenthesized_name_info = []
	if rtmss is not None:
		parenthesized_name_info.append("RTMSS = %s"%rtmss)
	if dtmss is not None:
		parenthesized_name_info.append("DTMSS = %s"%dtmss)
	if epcm:
		parenthesized_name_info.append("EPCM")
	if optimization_level != ATC_MAX_OPTIMIZATION_LEVEL:
		parenthesized_name_info.append("OPT %s"%optimization_level)
	if not blas:
		parenthesized_name_info.append("no BLAS")
	if lpmt is not None:
		parenthesized_name_info.append("LPMT = %s"%lpmt)
	if qrem is not None:
		parenthesized_name_info.append("QREM = %s"%qrem)
	if hwm is not None:
		parenthesized_name_info.append("HWM = %s"%hwm)
	if run != 0:
		parenthesized_name_info.append("run %s"%run)
	name = "ATC"
	if len(parenthesized_name_info) > 0:
		name += " (%s)"%(", ".join(parenthesized_name_info))
	return Compressor(name, INF_MAX_MODES, ["uint8", "uint16", "uint32", "uint64", "int8", "int16", "int32", "int64", "float32", "float64"], atc_generate_command, standard_correcting_psnr_estimate_target_psnr)

ATC = generate_atc_compressor()
compressors.append(ATC)
ATC_ALL_OPT_LEVELS = [generate_atc_compressor(optimization_level=level) for level in range(ATC_MAX_OPTIMIZATION_LEVEL)] # Does not include the max optimization level, which corresponds to ATC itself

# Video compressors

def generate_video_compressor(codec):
	
	# Define command generation function using codec
	def video_compressor_generate_command(mode, dataset, target_error=None, threads=1):
		
		command = [PYTHON3, "../video-compressors/%s_compressor.py"%codec]
		
		# Files
		command.extend([
			"-c", dataset.path, COMPRESSED_PATH
		] if mode == COMPRESS else [
			"-d", COMPRESSED_PATH, DECOMPRESSED_PATH
		])
		
		# Compression information
		if mode == COMPRESS:
			
			command.append(str(target_error)) # CRF
			command.append(dataset.dtype) # dtype
			command.append(str(dataset.most_constant_mode)) # Time mode index
			if codec == "x265":
				# This is not inherent to x265, but do to some new problems with maximum resolutions we explicitly specify the spatial modes and collapse the remaining modes in a different way
				for spatial_mode in dataset.spatial_modes:
					command.append(str(spatial_mode))
			command.extend(list(map(str, dataset.sizes))) # Sizes (MSI first)
			command.extend(["-p", "veryslow"]) # Preset
		
		command.extend(["-t", str(threads)])
		
		return command

	def video_compressor_estimate_target_error(target_psnr, data_range=None, dataset=None, prev_target_error=None, prev_psnr=None):
		
		# target error 0 means lossless compression (but still leads to a significant error due to rounding)
			
		if prev_target_error is None or prev_target_error == "lossless":
			# No previous compression, make a "blind" guess
			return 0
		else:
			# Just extrapolate based on the previous compression result
			# Increasing CRF by 6 halves bitrate, so let's assume it doubles the error as well
			return max(0, min(51, round(prev_target_error + 6.0*(prev_psnr - target_psnr)/20*log2(10))))

	def video_compressor_get_valid_target_errors(dataset):
		return list(range(51, -1, -1)) + (["lossless",] if codec == "x265" else [])
	
	return Compressor(codec, INF_MAX_MODES, ["uint8", "uint16", "uint32", "uint64", "int8", "int16", "int32", "int64", "float32", "float64"], video_compressor_generate_command, video_compressor_estimate_target_error, get_valid_target_errors=video_compressor_get_valid_target_errors)

x264 = generate_video_compressor("x264")
x265 = generate_video_compressor("x265")
compressors.append(x265)

# END OF LIST OF COMPRESSORS
def compress_or_decompress(mode, compressor, dataset, target_error=None, threads=1, measure_time=True, measure_memory=True, print_commands=False):
	
	# Checks
	if dataset.modes > compressor.max_modes:
		raise Exception("%s CLI only supports %s modes or less, not %s"%(compressor.name, compressor.max_modes, dataset.modes))
	if dataset.dtype not in compressor.supported_dtypes:
		raise Exception("%s CLI does not support data type %s. Supported data types: %s"%(compressor.name, dataset.dtype, compressor.supported_dtypes))
	
	# Generate command
	generate_command_result = compressor.generate_command(mode, dataset, target_error=target_error, threads=threads)
	log_path = "../logs/" + compressor.name.replace(" ", "_") + "_" + ("compress" if mode == COMPRESS else "decompress") + "_{date:%Y-%m-%d_%H:%M:%S}.txt".format(date=datetime.datetime.now())
	if type(generate_command_result) is tuple:
		command, env = generate_command_result # Special case needed to handle modified environment, such as with TuckerMPI
	else:
		command = generate_command_result
		env = None
	if print_commands:
		print(" ".join(command) + " > " + log_path)
	
	with open(log_path, "w") as f:
		if measure_time or measure_memory:
			results = measure_time_and_memory_usage(command, env=env, stdout_file_descriptor=f, measure_time=measure_time, measure_memory=measure_memory)
		else:
			# Simply execute command and pipe output to log
			p = Popen(command, stdout=f)
			p.wait()
			results = {}
	
	# Check if file was properly produced (file exists and has non-zero size)
	# If so, during compression, store compressed size and compression factor
	if mode == COMPRESS:
		if not (os.path.isfile(COMPRESSED_PATH) or os.path.isdir(COMPRESSED_PATH)) or os.path.getsize(COMPRESSED_PATH) == 0:
			raise Exception("%s did not produce the requested compressed file(s)!"%compressor.name)
		if os.path.isfile(COMPRESSED_PATH):
			results["compressed_size"] = os.path.getsize(COMPRESSED_PATH)
		elif os.path.isdir(COMPRESSED_PATH):
			# From https://stackoverflow.com/questions/1392413/calculating-a-directorys-size-using-python/1392549
			results["compressed_size"] = sum(os.path.getsize(COMPRESSED_PATH + "/" + f) for f in os.listdir(COMPRESSED_PATH) if os.path.isfile(COMPRESSED_PATH + "/" + f))
		if results["compressed_size"] == 0:
			raise Exception("%s did not produce the requested compressed file(s) (compressed size: 0)!"%compressor.name)
		results["compression_factor"] = np.dtype(dataset.original_dtype if dataset.original_dtype is not None else dataset.dtype).itemsize*reduce(lambda x, y: x*y, dataset.sizes)/results["compressed_size"]
	else:
		if not (os.path.isfile(DECOMPRESSED_PATH) or os.path.isdir(DECOMPRESSED_PATH)) or os.path.getsize(DECOMPRESSED_PATH) == 0:
			raise Exception("%s did not produce the requested decompressed file!"%compressor.name)
	
	return results

def compress(compressor, dataset, target_error, threads=1, measure_time=True, measure_memory=True, print_commands=False):
	return compress_or_decompress(COMPRESS, compressor, dataset, target_error=target_error, threads=threads, measure_time=measure_time, measure_memory=measure_memory, print_commands=print_commands)

def decompress(compressor, dataset, threads=1, measure_time=True, measure_memory=True, print_commands=False):
	return compress_or_decompress(DECOMPRESS, compressor, dataset, target_error=None, threads=threads, measure_time=measure_time, measure_memory=measure_memory, print_commands=print_commands)
