diff --git a/cellpack/autopack/Gradient.py b/cellpack/autopack/Gradient.py index 4d6478ccf..5db5eba48 100644 --- a/cellpack/autopack/Gradient.py +++ b/cellpack/autopack/Gradient.py @@ -46,9 +46,11 @@ # TODO: fix the save/restore grid """ -import numpy -from random import random import bisect +from random import random + +import numpy + from cellpack.autopack.utils import get_distances_from_point @@ -104,13 +106,12 @@ def update_ingredient_gradient(ingr, arguments): return ingr @staticmethod - def scale_between_0_and_1(values): + def normalize_by_max_value(values): """ - Scale values between 0 and 1 + Normalize values by their maximum value """ max_value = numpy.nanmax(values) - min_value = numpy.nanmin(values) - return (values - min_value) / (max_value - min_value) + return (values / max_value) if max_value != 0 else values @staticmethod def get_combined_gradient_weight(gradient_list, gradient_weights=None): @@ -131,10 +132,17 @@ def get_combined_gradient_weight(gradient_list, gradient_weights=None): weight_list = numpy.zeros((len(gradient_list), len(gradient_list[0].weight))) for i in range(len(gradient_list)): - weight_list[i] = Gradient.scale_between_0_and_1(gradient_list[i].weight) + weight_list[i] = Gradient.normalize_by_max_value(gradient_list[i].weight) + + if isinstance(gradient_weights, dict): + total = sum(gradient_weights.values()) + gradient_weights = [ + gradient_weights.get(gradient.name, 0) / total + for gradient in gradient_list + ] combined_weight = numpy.average(weight_list, axis=0, weights=gradient_weights) - combined_weight = Gradient.scale_between_0_and_1(combined_weight) + combined_weight = Gradient.normalize_by_max_value(combined_weight) return combined_weight @@ -158,7 +166,7 @@ def pick_point_from_weight(weight, points): """ weights_to_use = numpy.take(weight, points) weights_to_use[numpy.isnan(weights_to_use)] = 0 - weights_to_use = Gradient.scale_between_0_and_1(weights_to_use) + weights_to_use = Gradient.normalize_by_max_value(weights_to_use) point_probabilities = weights_to_use / numpy.sum(weights_to_use) @@ -253,6 +261,8 @@ def build_weight_map(self, bb, master_grid_positions): self.build_radial_weight_map(bb, master_grid_positions) elif self.mode == "surface": self.build_surface_distance_weight_map() + elif self.mode == "uniform": + self.build_uniform_weight_map(master_grid_positions) def get_gauss_weights(self, number_of_points, degree=5): """ @@ -317,9 +327,16 @@ def build_axis_weight_map(self, bb, master_grid_positions): self.distances = numpy.abs((master_grid_positions[:, ind] - mini)) self.set_weights_by_mode() + def build_uniform_weight_map(self, master_grid_positions): + """ + Build a uniform weight map + """ + self.distances = numpy.zeros(len(master_grid_positions), dtype=numpy.uint8) + self.weight = numpy.ones(len(master_grid_positions)) + def set_weights_by_mode(self): - self.scaled_distances = Gradient.scale_between_0_and_1(self.distances) + self.scaled_distances = Gradient.normalize_by_max_value(self.distances) if (numpy.nanmax(self.scaled_distances) > 1.0) or ( numpy.nanmin(self.scaled_distances) < 0.0 @@ -342,11 +359,17 @@ def set_weights_by_mode(self): "power" ] elif self.weight_mode == "exponential": - self.weight = numpy.exp( - -self.scaled_distances / self.weight_mode_settings["decay_length"] - ) + if self.weight_mode_settings["decay_length"] == 0: + self.weight = numpy.ones(len(self.scaled_distances)) + else: + self.weight = numpy.exp( + -self.scaled_distances / self.weight_mode_settings["decay_length"] + ) + else: + raise ValueError(f"Unknown weight mode: {self.weight_mode}") + # normalize the weight - self.weight = Gradient.scale_between_0_and_1(self.weight) + self.weight = Gradient.normalize_by_max_value(self.weight) if (numpy.nanmax(self.weight) > 1.0) or (numpy.nanmin(self.weight) < 0.0): raise ValueError( @@ -491,7 +514,7 @@ def create_voxelization(self, image_writer): ) if channel_values is None: continue - normalized_values = Gradient.scale_between_0_and_1(channel_values) + normalized_values = Gradient.normalize_by_max_value(channel_values) reshaped_values = numpy.reshape( normalized_values, image_writer.image_size, order="F" ) diff --git a/cellpack/autopack/__init__.py b/cellpack/autopack/__init__.py index 0ef25ef44..77ddaf465 100755 --- a/cellpack/autopack/__init__.py +++ b/cellpack/autopack/__init__.py @@ -150,27 +150,53 @@ def checkPath(): log.error(str(autopack_path_pref_file) + "file is not found") checkPath() -doit = False -if os.path.isfile(autopack_user_path_pref_file): - f = open(autopack_user_path_pref_file, "r") - doit = True -elif os.path.isfile(autopack_path_pref_file): - f = open(autopack_path_pref_file, "r") - doit = True -if doit: - log.info(f"autopack_path_pref_file {autopack_path_pref_file}") - pref_path = json.load(f) - f.close() - if "autoPACKserver" not in pref_path: - log.warning(f"problem with autopack_path_pref_file {autopack_path_pref_file}") - else: - autoPACKserver = pref_path["autoPACKserver"] - if "filespath" in pref_path: - if pref_path["filespath"] != "default": - filespath = pref_path["filespath"] - if "autopackdir" in pref_path: - if pref_path["autopackdir"] != "default": - autopackdir = pref_path["autopackdir"] + +def load_path_preferences(): + """Load path preferences from user or default preference files.""" + global autoPACKserver, filespath, autopackdir + + # Determine which preference file to use + pref_file = None + if os.path.isfile(autopack_user_path_pref_file): + pref_file = autopack_user_path_pref_file + elif os.path.isfile(autopack_path_pref_file): + pref_file = autopack_path_pref_file + + if pref_file is None: + log.warning("No preference files found") + return + + try: + with open(pref_file, "r") as f: + content = f.read().strip() + if not content: + log.warning(f"Preference file {pref_file} is empty") + return + + pref_path = json.loads(content) + + if not isinstance(pref_path, dict): + log.warning(f"Invalid preference file format in {pref_file}") + return + + if "autoPACKserver" not in pref_path: + log.warning(f"Missing 'autoPACKserver' key in {pref_file}") + else: + autoPACKserver = pref_path["autoPACKserver"] + + if "filespath" in pref_path and pref_path["filespath"] != "default": + filespath = pref_path["filespath"] + + if "autopackdir" in pref_path and pref_path["autopackdir"] != "default": + autopackdir = pref_path["autopackdir"] + + except json.JSONDecodeError as e: + log.error(f"Failed to parse JSON in {pref_file}: {e}") + except Exception as e: + log.error(f"Error loading preferences from {pref_file}: {e}") + + +load_path_preferences() REPLACE_PATH = { diff --git a/cellpack/autopack/ingredient/Ingredient.py b/cellpack/autopack/ingredient/Ingredient.py index d39d03620..c8f01faf9 100644 --- a/cellpack/autopack/ingredient/Ingredient.py +++ b/cellpack/autopack/ingredient/Ingredient.py @@ -425,7 +425,7 @@ def validate_ingredient_info(ingredient_info): if isinstance(ingredient_info["gradient"], list): if "gradient_weights" in ingredient_info: # check if gradient_weights are missing - if not isinstance(ingredient_info["gradient_weights"], list): + if not isinstance(ingredient_info["gradient_weights"], list | dict): raise Exception( f"Invalid gradient weights for ingredient {ingredient_info['name']}" ) diff --git a/cellpack/autopack/interface_objects/archive/gradient_data.py b/cellpack/autopack/interface_objects/archive/gradient_data.py index 4371ecb17..3bcdc5a77 100644 --- a/cellpack/autopack/interface_objects/archive/gradient_data.py +++ b/cellpack/autopack/interface_objects/archive/gradient_data.py @@ -21,6 +21,7 @@ class GradientModes(MetaEnum): VECTOR = "vector" RADIAL = "radial" SURFACE = "surface" + UNIFORM = "uniform" class WeightModes(MetaEnum): diff --git a/cellpack/autopack/upy/simularium/simularium_helper.py b/cellpack/autopack/upy/simularium/simularium_helper.py index 3908752bb..8801489bc 100644 --- a/cellpack/autopack/upy/simularium/simularium_helper.py +++ b/cellpack/autopack/upy/simularium/simularium_helper.py @@ -347,8 +347,11 @@ def add_grid_data_to_scene(self, incoming_name, positions, values, radius=0.5): positions, values = self.sort_values(positions, values) - normalized_values = (values - np.min(values)) / ( - np.max(values) - np.min(values) + max_value = np.nanmax(values) + min_value = np.nanmin(values) + value_range = max_value - min_value + normalized_values = ( + (values - min_value) / value_range if value_range != 0 else values ) colormap = matplotlib.cm.Reds(normalized_values) diff --git a/cellpack/autopack/validation/recipe_models.py b/cellpack/autopack/validation/recipe_models.py index fcfc3397e..d4ffa603b 100644 --- a/cellpack/autopack/validation/recipe_models.py +++ b/cellpack/autopack/validation/recipe_models.py @@ -50,6 +50,7 @@ class GradientMode(str, Enum): VECTOR = "vector" RADIAL = "radial" SURFACE = "surface" + UNIFORM = "uniform" class WeightMode(str, Enum): @@ -134,7 +135,7 @@ class WeightModeOptions(str, Enum): class WeightModeSettings(BaseModel): - decay_length: Optional[float] = Field(None, gt=0) + decay_length: Optional[float] = Field(None, ge=0) power: Optional[float] = Field(None, gt=0) @@ -284,7 +285,7 @@ class RecipeObject(BaseModel): partners: Optional[Union[List[Partner], Dict[str, Any]]] = None # Gradient field supports multiple formats: # - str: Simple reference to gradient name (standard format) - # - List[str]: List of gradient names (for multiple gradients) + # - List[str] OR dict[str, dict[str, Any]]: List of gradient names (for multiple gradients) # - RecipeGradient: Full gradient definition (for unnested Firebase recipes) # - List[RecipeGradient]: List of full gradient definitions (for unnested Firebase recipes) # @@ -294,7 +295,13 @@ class RecipeObject(BaseModel): # Unnested Firebase: {"name": "gradient_name", "mode": "surface", ...} # Converted Firebase list: [{"name": "grad1", "mode": "X"}, {"name": "grad2", "mode": "Y"}] gradient: Optional[ - Union[str, List[str], "RecipeGradient", List["RecipeGradient"]] + Union[ + str, + list[str], + dict[str, dict[str, Any]], + "RecipeGradient", + list["RecipeGradient"], + ] ] = None weight: Optional[float] = Field(None, ge=0) is_attractor: Optional[bool] = None diff --git a/cellpack/autopack/writers/ImageWriter.py b/cellpack/autopack/writers/ImageWriter.py index c429cb537..f0f8fd270 100644 --- a/cellpack/autopack/writers/ImageWriter.py +++ b/cellpack/autopack/writers/ImageWriter.py @@ -1,9 +1,12 @@ +import logging from pathlib import Path import numpy from bioio_ome_tiff.writers import OmeTiffWriter from scipy.ndimage import convolve +log = logging.getLogger(__name__) + """ ImageWriter provides a class to export cellpack packings as tiff images """ @@ -209,7 +212,7 @@ def export_image(self): """ Saves the results as a tiff file """ - print(f"Exporting image to {self.output_path}") + log.debug(f"Exporting image to {self.output_path}") ( concatenated_image, channel_names, diff --git a/cellpack/autopack/writers/__init__.py b/cellpack/autopack/writers/__init__.py index f98a5c590..14b86ad69 100644 --- a/cellpack/autopack/writers/__init__.py +++ b/cellpack/autopack/writers/__init__.py @@ -5,12 +5,13 @@ import json import os -import numpy from collections import OrderedDict +import numpy + +import cellpack.autopack.transformation as tr from cellpack import autopack from cellpack.autopack.ingredient.grow import ActinIngredient, GrowIngredient -import cellpack.autopack.transformation as tr def updatePositionsRadii(ingr): diff --git a/cellpack/bin/validate.py b/cellpack/bin/validate.py index 7a419cb8b..32865a00a 100644 --- a/cellpack/bin/validate.py +++ b/cellpack/bin/validate.py @@ -1,10 +1,11 @@ import logging import logging.config -import fire from pathlib import Path -from cellpack.autopack.loaders.recipe_loader import RecipeLoader +import fire + from cellpack.autopack.interface_objects.database_ids import DATABASE_IDS +from cellpack.autopack.loaders.recipe_loader import RecipeLoader ############################################################################### log_file_path = Path(__file__).parent.parent / "logging.conf" @@ -35,7 +36,8 @@ def validate(recipe_path): "Remote database not initialized. Please set up credentials for the database." ) log.error( - "See: https://github.com/mesoscope/cellpack?tab=readme-ov-file#introduction-to-remote-databases" + "See: https://github.com/mesoscope/cellpack?tab=readme-ov-file" + "#introduction-to-remote-databases" ) else: log.error(f"Error loading recipe: {e}") diff --git a/cellpack/tests/recipes/v2/test_gradient.json b/cellpack/tests/recipes/v2/test_gradient.json new file mode 100644 index 000000000..45bb727d0 --- /dev/null +++ b/cellpack/tests/recipes/v2/test_gradient.json @@ -0,0 +1,124 @@ +{ + "version": "default", + "format_version": "2.0", + "name": "test_gradient", + "bounding_box": [ + [ + -100, + -100, + 0 + ], + [ + 100, + 100, + 1 + ] + ], + "gradients": { + "radial_gradient": { + "mode": "radial", + "description": "Radial gradient from the center", + "weight_mode": "cube", + "pick_mode": "rnd", + "mode_settings": { + "direction": [ + 0, + 0, + 0 + ], + "radius": 150, + "center": [ + 0, + 0, + 0 + ] + } + }, + "zero_decay": { + "mode": "radial", + "description": "Radial gradient with zero decay", + "weight_mode": "exponential", + "pick_mode": "rnd", + "weight_mode_settings": { + "decay_length": 0.0 + }, + "mode_settings": { + "direction": [ + 0, + 0, + 0 + ], + "radius": 150, + "center": [ + 0, + 0, + 0 + ] + } + }, + "vector_gradient": { + "mode": "vector", + "description": "Gradient away from the plane formed by center and vector", + "weight_mode": "cube", + "pick_mode": "rnd", + "mode_settings": { + "direction": [ + 1, + 1, + 0 + ] + } + }, + "uniform_gradient": { + "mode": "uniform", + "description": "Uniform gradient" + } + }, + "objects": { + "base": { + "packing_mode": "gradient", + "principal_vector": [ + 0, + 0, + 1 + ], + "place_method": "jitter", + "jitter_attempts": 100, + "available_regions": { + "interior": {}, + "surface": {}, + "outer_leaflet": {}, + "inner_leaflet": {} + } + }, + "sphere": { + "type": "single_sphere", + "inherit": "base", + "color": [ + 0.5, + 0.5, + 0.5 + ], + "gradient": "radial_gradient", + "radius": 5, + "max_jitter": [ + 1, + 1, + 0 + ] + } + }, + "composition": { + "space": { + "regions": { + "interior": [ + "A" + ] + } + }, + "A": { + "object": "sphere", + "count": 100 + } + } +} \ No newline at end of file diff --git a/uv.lock b/uv.lock index 48582665f..48d827f46 100644 --- a/uv.lock +++ b/uv.lock @@ -244,7 +244,6 @@ dev = [ ] docs = [ { name = "linkify-it-py" }, - { name = "mdutils" }, { name = "myst-parser" }, ] lint = [ @@ -297,7 +296,6 @@ dev = [ ] docs = [ { name = "linkify-it-py" }, - { name = "mdutils" }, { name = "myst-parser" }, ] lint = [