Project: s41467-024-47677-8 | Function: double_hull (python)

system: You are an expert Python programmer implementing algorithms from scientific papers.
Complete the masked function based on the algorithm description.
Follow Python best practices and maintain consistent code style.
Return **only** the complete Python function code.

user: Complete this Python function:

**DESCRIPTION:** Dual convex hull algorithm for calculating inner and outer cluster boundaries, computing both external hull area and internal hull area to quantify nanocluster spatial organization in single-molecule localization data

**KEY TERMS:** {
  "ext_hull": "external convex hull encompassing all cluster points",
  "int_hull": "internal convex hull of core cluster points",
  "ext_area": "total cluster boundary area",
  "int_area": "core cluster area used for radius calculation",
  "vertices": "convex hull vertex indices",
  "ConvexHull": "scipy computational geometry algorithm"
}

**CONTEXT CODE:** Generated .trxyt files [1] and corresponding metrics.tsv files [2] are saved within the same directory as the  synthetic_data_generator_gui.py script, in a folder that is created with the naming format: synthetic_data_output_YYYYMMDD-HHMMSS

USAGE: 
1 - Copy this script to the top level directory containing the files you are interested in, and run it (either by double clicking or navigating to the location of the script in the terminal and using the command 'python synthetic_data_generator_cli.py')
2 - Press x followed by the enter key to define the selection area using a pretend x,y frame size, or press n followed by the enter key to define the selection area using a previously saved NASTIC roi_coordinates.tsv file
3 - Using NASTIC ROI file: A list of roi_coordinates.tsv files found in the current directory and all subdirectories will be generated
4 - Using NASTIC ROI file: press the number corresponding to the roi_coordinates.tsv file that you wish to use as the ROI followed by the enter key
5 - Generated .trxyt files and metrics.tsv files will be saved in a datestamped folder
6 - Hit the return key to generate another file, or press Ctrl-c to exit the program

CHECK FOR UPDATES:
https://github.com/tristanwallis/smlm_clustering/releases
'''

last_changed = "20250701"

# LOAD MODULES 
import numpy as np
import os
import glob
import random
import datetime
from scipy.spatial import ConvexHull
import math
from matplotlib import path

# VARS
acquisition_time = 320 # pretend length of acquisition (default 320)
frame_time = 0.02 # sec (default 0.02)
seed_density = 2 # how many cluster seeds per um2 of selection area (default 2)
min_traj_num = 4 # min number of trajectories around each seed (default 4)
max_traj_num = 12 # max number of trajectories around each seed (default 12)
x_size = 10 # pretend microns (default 10)
y_size = 10 # pretend microns (default 10)
min_traj_length = 5 # min number of trajectory steps (default 5)
max_traj_length = 30 # max number of trajectory steps (default 30)
radius = 0.1 # radius around each seed to make trajectories (um) (default 0.1)
steplength = 0.1 # maximum step length within trajectory (um) (default 0.1)
noise_factor = 5 # number of unclustered noise trajectories per seed (default 5)
unconst = 4 # steplength multiplier of unclustered trajectories (default 4)
hotspotprobability = 0.2 # chance of a given seed point generating multiple spatially overlapping but temporally distinct clusters (default 0.2)
hotspotmax = 3 # maximum number of temporal clusters at a given hotspot (default 3)
orbit = True # clustered trajectories orbit their spawn point rather than random walking (default True)
trxyt_num = 1 # number of trxyt files to generate (default 1)

clusterdict = {} # dictionary holding clusters

# FUNCTIONS

# CONVEX HULL OF EXTERNAL POINTS, AND THEN INTERNAL POINTS
def double_hull(points):
	"""Dual convex hull algorithm for calculating inner and outer cluster boundaries, computing both external hull area and internal hull area to quantify nanocluster spatial organization in single-molecule localization data

	:param points: List of coordinate points [(x1,y1), (x2,y2), ...]
	:return: Tuple containing (ext_x, ext_y, ext_area, int_x, int_y, int_area)
	"""
	# MASKED: Dual convex hull algorithm for calculating inner and outer cluster boundaries, computing both external hull area and internal hull area to quantify nanocluster spatial organization in single-molecule localization data
	raise NotImplementedError("This function is masked for testing purposes.")

# ROI AREA	
def PolyArea(x,y):
	return 0.5*np.abs(np.dot(x,np.roll(y,1))-np.dot(y,np.roll(x,1)))	

# OBTAIN POINTS BETWEEN TWO TRAJECTORY COORDINATES
def obtain_midpoints(trajx_before,trajy_before,x,y):
	midpoint_x_list = []
	midpoint_y_list = []
	midpoint_x = (x-trajx_before)/7
	midpoint_y = (y-trajy_before)/7
	for i in range(1,7):
		point_x = trajx_before + (i*midpoint_x)
		midpoint_x_list.append(point_x)
		point_y = trajy_before + (i*midpoint_y)
		midpoint_y_list.append(point_y)
	midpoint_list = zip(midpoint_x_list,midpoint_y_list)
	return(midpoint_list)
		
# READ ROI FILE	
def read_roi():
	global allx, ally, selverts, roidict,p_list,selarea_list
	print ("\nLoading NASTIC ROI file")
	print("------------------------------------------------------------")
	print("NASTIC ROI file selected: \n{}".format(roi_file))
	print("Reading ROI file...")
	roidict = {}
	allx = []
	ally = []
	with open (roi_file,"r") as infile:
		for line in infile:
			spl = line.split("\t")
			try:
				roi = int(spl[0])
				x = float(spl[1])
				y = float(spl[2])
				if x < 0:
					x = 0.0 # prevent crash if roi file contains negative x values
				if y < 0:
					y = 0.0 # prevent crash if roi file contains negative y values
				allx.append(x)
				ally.append(y)
				try:
					roidict[roi].append([x,y])
				except:	
					roidict[roi] = []
					roidict[roi].append([x,y])
			except:
				pass
	p_list = [] # list of ROI paths (1 per ROI in ROI file)
	selarea_list = [] # list of ROI selection areas (1 per ROI in ROI file)
	roi_ct = 0
	for roi in roidict:
		roi_ct+=1
		selverts =roidict[roi]
		p = path.Path(selverts)
		p_list.append(p)
		selarea =PolyArea(list(zip(*selverts))[0],list(zip(*selverts))[1])
		selarea_list.append(selarea)
	print("{} ROIs found in ROI file\n".format(roi_ct))
	return()

# GENERATE TRXYT
def generate_trxyt(Load_ROI):
	"""Comprehensive synthetic trajectory generator implementing spatiotemporal cluster formation with constrained random walks, hotspot generation, and noise trajectories for validating NASTIC clustering algorithms

	:param Load_ROI: Boolean flag to determine if ROI coordinates are used for area definition
	"""
	# MASKED: Comprehensive synthetic trajectory generator implementing spatiotemporal cluster formation with constrained random walks, hotspot generation, and noise trajectories for validating NASTIC clustering algorithms
	raise NotImplementedError("This function is masked for testing purposes.")
	
# Output
def output(traj_num_ct,stamp,Load_ROI):								 
	print("\nWriting files")
	print("------------------------------------------------------------")
	print ("Writing trxyt...")
	with open("synthetic_data_output_{}/synthetic_data_{}_{}.trxyt".format(stamp, stamp,traj_num_ct+1),"a") as outfile: 

		if Load_ROI == False:
			for tr,traj in enumerate(trajectories,start=1):
				for seg in traj:
					x,y,t = seg
					outline = "{} {} {} {}\n".format(tr,x,y,round(t,frame_time_round_val))
					outfile.write(outline)
		elif Load_ROI == True:
			if len(trajectory_list) > 1:
				trajectory_temp_list = []
				for tr, trajectory in enumerate(trajectory_list):
					for traj in trajectory:
						trajectory_temp_list.append(traj)
				for tr,traj in enumerate(trajectory_temp_list,start=1):
					for seg in traj:


Complete the function implementation replacing "raise NotImplementedError".