Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions cellpack/autopack/interface_objects/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@
"""Interface classes for cellpack. These are to establish data structures."""
from .default_values import ( # noqa: F401
default_recipe_values,
DEFAULT_GRADIENT_MODE_SETTINGS,
)
from .gradient_data import GradientData # noqa: F401
from .ingredient_types import INGREDIENT_TYPE # noqa: F401
from .representations import Representations # noqa: F401

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@

from cellpack.autopack.utils import deep_merge

from .default_values import DEFAULT_GRADIENT_MODE_SETTINGS
from .meta_enum import MetaEnum
from ...validation.recipe_models import DEFAULT_GRADIENT_MODE_SETTINGS
from ..meta_enum import MetaEnum

"""
GradientData provides a class to pass sanitized arguments to create gradients
Expand Down
10 changes: 0 additions & 10 deletions cellpack/autopack/interface_objects/default_values.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,6 @@
"representations": {"atomic": None, "packing": None, "mesh": None},
}

DEFAULT_GRADIENT_MODE_SETTINGS = {
"mode": "X",
"weight_mode": "linear",
"pick_mode": "linear",
"description": "Linear gradient in the X direction",
"reversed": False, # is the direction of the vector reversed?
"invert": None, # options: "weight", "distance"
"mode_settings": {},
"weight_mode_settings": {},
}
Comment on lines -6 to -15
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moving the constant to recipe model to keep things centralized


default_firebase_collection_names = [
"composition",
Expand Down
8 changes: 5 additions & 3 deletions cellpack/autopack/loaders/migrate_v2_to_v2_1.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import copy

from ..interface_objects.gradient_data import GradientData, ModeOptions
from ..validation.recipe_models import DEFAULT_GRADIENT_MODE_SETTINGS, ModeOptions


def convert_partners(object_data):
Expand Down Expand Up @@ -33,11 +33,13 @@ def convert_partners(object_data):

def convert_gradients(old_gradients_dict):
new_gradients_dict = {}
mode_setting_keys = [option.value for option in ModeOptions]

for gradient_name, gradient_dict in old_gradients_dict.items():
gradient_data = copy.deepcopy(GradientData.default_values)
gradient_data = copy.deepcopy(DEFAULT_GRADIENT_MODE_SETTINGS)

for key, value in gradient_dict.items():
if ModeOptions.is_member(key):
if key in mode_setting_keys:
gradient_data["mode_settings"][key] = value
else:
gradient_data[key] = value
Expand Down
5 changes: 3 additions & 2 deletions cellpack/autopack/loaders/recipe_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

from cellpack.autopack.DBRecipeHandler import DBRecipeLoader
from cellpack.autopack.interface_objects import (
GradientData,
Representations,
default_recipe_values,
)
Expand Down Expand Up @@ -238,7 +237,9 @@ def _read(self, resolve_inheritance=True, use_docker=False):
):
gradients = []
for gradient_name, gradient_dict in recipe_data["gradients"].items():
gradients.append(GradientData(gradient_dict, gradient_name).data)
gradient_data = gradient_dict.copy()
gradient_data["name"] = gradient_name
gradients.append(gradient_data)
recipe_data["gradients"] = gradients
return recipe_data

Expand Down
162 changes: 124 additions & 38 deletions cellpack/autopack/validation/recipe_models.py
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

merging gradient constants, classes, and settings here from archived gradient_data.py

Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ class PlaceMethod(str, Enum):
SPHERES_SST = "spheresSST"


class CoordinateSystem(str, Enum):
LEFT = "left"
RIGHT = "right"


# 3-element float array - used for 3D vectors, colors, etc.
ThreeFloatArray = List[float]


# GRADIENT CLASSES
class GradientMode(str, Enum):
X = "X"
Y = "Y"
Z = "Z"
VECTOR = "vector"
RADIAL = "radial"
SURFACE = "surface"


class WeightMode(str, Enum):
LINEAR = "linear"
SQUARE = "square"
Expand All @@ -51,22 +70,67 @@ class PickMode(str, Enum):
REG = "reg"


class GradientMode(str, Enum):
X = "x"
Y = "y"
Z = "z"
VECTOR = "vector"
RADIAL = "radial"
SURFACE = "surface"
class ModeOptions(str, Enum):
"""
All available options for individual modes
"""

direction = "direction"
center = "center"
radius = "radius"
gblob = "gblob"
object = "object"
scale_distance_between = "scale_distance_between"

class CoordinateSystem(str, Enum):
LEFT = "left"
RIGHT = "right"

class InvertOptions(str, Enum):
"""
All available options for individual invert modes
"""

weight = "weight"
distance = "distance"


class WeightModeOptions(str, Enum):
"""
All available options for individual weight modes
"""

power = "power"
decay_length = "decay_length"

# 3-element float array - used for 3D vectors, colors, etc.
ThreeFloatArray = List[float]

REQUIRED_MODE_OPTIONS = {
GradientMode.VECTOR: [ModeOptions.direction],
GradientMode.SURFACE: [ModeOptions.object],
}


DIRECTION_MAP = {
GradientMode.X: [1, 0, 0],
GradientMode.Y: [0, 1, 0],
GradientMode.Z: [0, 0, 1],
}


REQUIRED_WEIGHT_MODE_OPTIONS = {
WeightMode.POWER: [WeightModeOptions.power],
WeightMode.EXPONENTIAL: [WeightModeOptions.decay_length],
}


# default gradient settings for v2.0 to v2.1 migration
DEFAULT_GRADIENT_MODE_SETTINGS = {
"mode": "X",
"weight_mode": "linear",
"pick_mode": "linear",
"description": "Linear gradient in the X direction",
"reversed": False,
"invert": None,
"mode_settings": {},
"weight_mode_settings": {},
}


class WeightModeSettings(BaseModel):
Expand All @@ -89,46 +153,68 @@ class RecipeGradient(BaseModel):
pick_mode: PickMode = Field(PickMode.LINEAR)
weight_mode: Optional[WeightMode] = None
reversed: Optional[bool] = None
invert: Optional[bool] = None
invert: Optional[InvertOptions] = None
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed invert type to be string to match the settings in gradient_data.py

weight_mode_settings: Optional[WeightModeSettings] = None
mode_settings: Optional[GradientModeSettings] = None

@model_validator(mode="after")
def validate_mode_requirements(self):
"""Validate that required `mode_settings` exist for modes that need them"""
# surface mode requires mode_settings with object
if self.mode == GradientMode.SURFACE:
if not self.mode_settings:
raise ValueError("Surface gradient mode requires 'mode_settings' field")
if (
not hasattr(self.mode_settings, "object")
or not self.mode_settings.object
):
raise ValueError(
"Surface gradient mode requires 'object' in mode_settings"
)
required_options = REQUIRED_MODE_OPTIONS.get(self.mode)

# vector mode requires mode_settings with direction
elif self.mode == GradientMode.VECTOR:
if required_options:
if not self.mode_settings:
raise ValueError("Vector gradient mode requires 'mode_settings' field")
if (
not hasattr(self.mode_settings, "direction")
or not self.mode_settings.direction
):
raise ValueError(
"Vector gradient mode requires 'direction' in mode_settings"
f"{self.mode.value} gradient mode requires 'mode_settings' field"
)

# validate that direction vector is not zero (only for vector mode)
import math
for option in required_options:
option_name = option.value if hasattr(option, "value") else option
if (
not hasattr(self.mode_settings, option_name)
or getattr(self.mode_settings, option_name) is None
):
raise ValueError(
f"{self.mode.value} gradient mode requires '{option_name}' in mode_settings"
)

# vector mode direction must be non-zero
if self.mode == GradientMode.VECTOR and self.mode_settings:
if self.mode_settings.direction:
import math

magnitude = math.sqrt(sum(x**2 for x in self.mode_settings.direction))
if magnitude == 0:
raise ValueError(
"Vector gradient mode requires a non-zero direction vector"
)

return self

@model_validator(mode="after")
def validate_weight_mode_requirements(self):
"""Validate that required `weight_mode_settings` exist for weight modes that need them"""
if self.weight_mode is None:
return self

required_options = REQUIRED_WEIGHT_MODE_OPTIONS.get(self.weight_mode)

magnitude = math.sqrt(sum(x**2 for x in self.mode_settings.direction))
if magnitude == 0:
if required_options:
if not self.weight_mode_settings:
raise ValueError(
"Vector gradient mode requires a non-zero direction vector"
f"{self.weight_mode.value} weight mode requires 'weight_mode_settings' field"
)

for option in required_options:
option_name = option.value if hasattr(option, "value") else option
if (
not hasattr(self.weight_mode_settings, option_name)
or getattr(self.weight_mode_settings, option_name) is None
):
raise ValueError(
f"{self.weight_mode.value} weight mode requires '{option_name}' in weight_mode_settings"
)

return self


Expand Down Expand Up @@ -206,7 +292,7 @@ class RecipeObject(BaseModel):
# Standard format: "gradient_name"
# Multiple gradients: ["gradient1", "gradient2"]
# Unnested Firebase: {"name": "gradient_name", "mode": "surface", ...}
# Converted Firebase list: [{"name": "grad1", "mode": "x"}, {"name": "grad2", "mode": "y"}]
# Converted Firebase list: [{"name": "grad1", "mode": "X"}, {"name": "grad2", "mode": "Y"}]
gradient: Optional[
Union[str, List[str], "RecipeGradient", List["RecipeGradient"]]
] = None
Expand Down
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to still have this test for gradient data with the new validation schema, but that can be a separate PR.

Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
ARCHIVED: This test file was for the old GradientData class which has been replaced
with pydantic validation models (RecipeGradient in recipe_models.py).

Docs: https://docs.pytest.org/en/latest/example/simple.html
https://docs.pytest.org/en/latest/plugins.html#requiring-loading-plugins-in-a-test-module-or-conftest-file
"""
import pytest
from cellpack.autopack.interface_objects import GradientData
from cellpack.autopack.interface_objects.archive.gradient_data import GradientData


@pytest.mark.parametrize(
Expand Down
2 changes: 1 addition & 1 deletion docs/RECIPE_SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ This section contains information about the objects to be packed, including thei
| `pick_mode` | string(enum) | Gradient sampling method | `"linear"` | Accepts a pick mode from `"max"`, `"min"`, `"rnd"`, `"linear"`, `"binary"`, `"sub"`, `"reg"` |
| `weight_mode` | string(enum) | Modulates the form of the grid point weight dropoff | | Accepts a weight mode from `"linear"`, `"square"`, `"cube"`, `"power"`, `"exponential"` |
| `reversed` | boolean | Reverse gradient direction | | |
| `invert` | boolean | Invert gradient weights | | |
| `invert` | string | Invert gradient weights | | |
| `weight_mode_settings.decay_length` | number | Decay rate parameter | | Controls gradient falloff |
| `mode_settings.object` | string | Reference object for gradient | | Links to a mesh object |
| `mode_settings.scale_to_next_surface` | boolean | Scaling toggle | | |
Expand Down