Coverage for openhcs/core/components/framework.py: 63.5%

54 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-01 18:33 +0000

1""" 

2Core framework for generic component configuration. 

3 

4This module provides the foundational classes for configuring any enum as components 

5with configurable multiprocessing axis and validation constraints. 

6""" 

7 

8from dataclasses import dataclass 

9from typing import Generic, TypeVar, Set, List, Optional, Type 

10from enum import Enum 

11 

12T = TypeVar('T', bound=Enum) 

13 

14 

15@dataclass(frozen=True) 

16class ComponentConfiguration(Generic[T]): 

17 """ 

18 Generic configuration for any enum-based component system. 

19  

20 This class encapsulates the configuration for a component system where: 

21 - Components are defined by an enum 

22 - One component serves as the multiprocessing axis 

23 - Default variable components and group_by are specified 

24 - Generic constraint validation is enforced: group_by ∉ variable_components 

25 """ 

26 

27 all_components: Set[T] 

28 multiprocessing_axis: T 

29 default_variable: List[T] 

30 default_group_by: Optional[T] 

31 

32 def __post_init__(self): 

33 """Validate configuration constraints.""" 

34 # Ensure multiprocessing_axis is in all_components 

35 if self.multiprocessing_axis not in self.all_components: 35 ↛ 36line 35 didn't jump to line 36 because the condition on line 35 was never true

36 raise ValueError( 

37 f"multiprocessing_axis {self.multiprocessing_axis.value} " 

38 f"must be in all_components" 

39 ) 

40 

41 # Ensure default_variable components are in all_components 

42 for component in self.default_variable: 

43 if component not in self.all_components: 43 ↛ 44line 43 didn't jump to line 44 because the condition on line 43 was never true

44 raise ValueError( 

45 f"default_variable component {component.value} " 

46 f"must be in all_components" 

47 ) 

48 

49 # Ensure default_group_by is in all_components (if specified) 

50 if self.default_group_by and self.default_group_by not in self.all_components: 50 ↛ 51line 50 didn't jump to line 51 because the condition on line 50 was never true

51 raise ValueError( 

52 f"default_group_by {self.default_group_by.value} " 

53 f"must be in all_components" 

54 ) 

55 

56 # Validate default combination 

57 self.validate_combination(self.default_variable, self.default_group_by) 

58 

59 def validate_combination(self, variable: List[T], group_by: Optional[T]) -> None: 

60 """ 

61 Validate that group_by is not in variable_components. 

62 

63 This enforces the core constraint: group_by ∉ variable_components 

64 

65 Args: 

66 variable: List of variable components 

67 group_by: Optional group_by component 

68 

69 Raises: 

70 ValueError: If group_by is in variable_components 

71 """ 

72 if group_by and group_by in variable: 72 ↛ 73line 72 didn't jump to line 73 because the condition on line 72 was never true

73 raise ValueError( 

74 f"group_by {group_by.value} cannot be in variable_components " 

75 f"{[v.value for v in variable]}" 

76 ) 

77 

78 def get_remaining_components(self) -> Set[T]: 

79 """ 

80 Get components available for variable_components and group_by selection. 

81 

82 Returns all components except the multiprocessing_axis. 

83 

84 Returns: 

85 Set of components available for variable/group_by selection 

86 """ 

87 return self.all_components - {self.multiprocessing_axis} 

88 

89 def get_available_variable_components(self) -> List[T]: 

90 """ 

91 Get all components that can be used as variable_components. 

92 

93 Returns: 

94 List of components available as variable components 

95 """ 

96 return list(self.get_remaining_components()) 

97 

98 def get_available_group_by_components(self, exclude_variable: Optional[List[T]] = None) -> List[T]: 

99 """ 

100 Get components that can be used as group_by, excluding variable components. 

101 

102 Args: 

103 exclude_variable: Variable components to exclude from group_by options 

104 

105 Returns: 

106 List of components available as group_by 

107 """ 

108 remaining = self.get_remaining_components() 

109 if exclude_variable: 

110 remaining = remaining - set(exclude_variable) 

111 return list(remaining) 

112 

113 

114class ComponentConfigurationFactory: 

115 """Factory for creating ComponentConfiguration instances.""" 

116 

117 @staticmethod 

118 def create_configuration( 

119 component_enum: Type[T], 

120 multiprocessing_axis: T, 

121 default_variable: Optional[List[T]] = None, 

122 default_group_by: Optional[T] = None 

123 ) -> ComponentConfiguration[T]: 

124 """ 

125 Create a ComponentConfiguration for the given enum with dynamic component resolution. 

126 

127 When multiprocessing_axis is specified, the remaining components are automatically 

128 available for variable_components and group_by selection. 

129 

130 Args: 

131 component_enum: The enum class defining all components 

132 multiprocessing_axis: Component to use for multiprocessing 

133 default_variable: Default variable components (auto-resolved if None) 

134 default_group_by: Default group_by component (auto-resolved if None) 

135 

136 Returns: 

137 ComponentConfiguration instance 

138 """ 

139 all_components = set(component_enum) 

140 

141 # Dynamic resolution: remaining components = all_components - multiprocessing_axis 

142 remaining_components = all_components - {multiprocessing_axis} 

143 

144 # Auto-resolve default_variable if not specified 

145 if default_variable is None: 145 ↛ 147line 145 didn't jump to line 147 because the condition on line 145 was never true

146 # Use the first remaining component as default variable 

147 default_variable = [list(remaining_components)[0]] if remaining_components else [] 

148 

149 # Auto-resolve default_group_by if not specified 

150 if default_group_by is None and len(remaining_components) > 1: 150 ↛ 152line 150 didn't jump to line 152 because the condition on line 150 was never true

151 # Use the second remaining component as default group_by (if available) 

152 remaining_list = list(remaining_components) 

153 # Ensure group_by is not in default_variable 

154 for component in remaining_list: 

155 if component not in default_variable: 

156 default_group_by = component 

157 break 

158 

159 return ComponentConfiguration( 

160 all_components=all_components, 

161 multiprocessing_axis=multiprocessing_axis, 

162 default_variable=default_variable, 

163 default_group_by=default_group_by 

164 ) 

165 

166 @staticmethod 

167 def create_openhcs_default_configuration(): 

168 """ 

169 Create the default OpenHCS configuration. 

170 

171 This maintains backward compatibility with the current OpenHCS setup: 

172 - Well as multiprocessing axis 

173 - Site as default variable component 

174 - Channel as default group_by 

175 """ 

176 # Import here to avoid circular import with constants.py 

177 from enum import Enum 

178 

179 class VariableComponents(Enum): 

180 SITE = "site" 

181 CHANNEL = "channel" 

182 Z_INDEX = "z_index" 

183 WELL = "well" 

184 

185 return ComponentConfigurationFactory.create_configuration( 

186 VariableComponents, 

187 multiprocessing_axis=VariableComponents.WELL, 

188 default_variable=[VariableComponents.SITE], 

189 default_group_by=VariableComponents.CHANNEL 

190 )