Coverage for src/seqrule/rulesets/tea.py: 55%

115 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-02-27 10:39 -0600

1""" 

2Tea Processing Rules. 

3 

4This module implements sequence rules for tea processing, with support for: 

5- Different tea types and their processing requirements 

6- Temperature and humidity controls 

7- Processing step durations and order 

8- Oxidation and fermentation levels 

9- Quality control parameters 

10- Regional variations 

11 

12Common use cases: 

13- Validating traditional processing methods 

14- Ensuring quality control standards 

15- Managing tea factory workflows 

16- Documenting regional variations 

17- Training tea processors 

18""" 

19 

20from dataclasses import dataclass 

21from enum import Enum 

22from typing import Dict, Optional 

23 

24from ..core import AbstractObject, Sequence 

25from ..dsl import DSLRule 

26 

27 

28class TeaType(Enum): 

29 """Types of tea based on processing method.""" 

30 

31 GREEN = "green" # Unoxidized 

32 WHITE = "white" # Slightly oxidized 

33 YELLOW = "yellow" # Slightly oxidized, unique withering 

34 OOLONG = "oolong" # Partially oxidized 

35 BLACK = "black" # Fully oxidized 

36 PUERH = "puerh" # Post-fermented 

37 DARK = "dark" # Post-fermented (non-puerh) 

38 

39 

40class ProcessingStep(Enum): 

41 """Steps in tea processing.""" 

42 

43 PLUCKING = "plucking" # Leaf harvesting 

44 WITHERING = "withering" # Moisture reduction 

45 BRUISING = "bruising" # Cell disruption 

46 ROLLING = "rolling" # Leaf shaping 

47 OXIDATION = "oxidation" # Enzymatic browning 

48 FIXING = "fixing" # Enzyme deactivation 

49 FERMENTATION = "fermentation" # Microbial fermentation 

50 DRYING = "drying" # Final moisture removal 

51 AGING = "aging" # Post-processing aging 

52 

53 

54@dataclass 

55class QualityMetrics: 

56 """Quality control metrics for tea processing.""" 

57 

58 moisture_content: float # Percentage 

59 leaf_integrity: float # 0-1 scale 

60 color_value: float # L*a*b* color space 

61 aroma_intensity: float # 0-1 scale 

62 taste_profile: Dict[str, float] # Flavor wheel values 

63 

64 

65class TeaProcess(AbstractObject): 

66 """ 

67 A tea processing step with properties. 

68 

69 Properties: 

70 tea_type: Type of tea being processed 

71 step: Processing step being performed 

72 temperature: Temperature in Celsius 

73 duration: Duration in hours 

74 humidity: Relative humidity percentage 

75 leaf_ratio: Leaf to water ratio (if applicable) 

76 quality: Quality metrics at this step 

77 """ 

78 

79 def __init__( 

80 self, 

81 tea_type: TeaType, 

82 step: ProcessingStep, 

83 temperature: Optional[float] = None, 

84 duration: Optional[float] = None, 

85 humidity: Optional[float] = None, 

86 leaf_ratio: Optional[float] = None, 

87 quality: Optional[QualityMetrics] = None, 

88 ): 

89 """Initialize a tea processing step.""" 

90 if temperature is not None and temperature < 0: 

91 raise ValueError("Temperature cannot be negative") 

92 

93 if duration is not None and duration < 0: 

94 raise ValueError("Duration cannot be negative") 

95 

96 if humidity is not None and not (0 <= humidity <= 100): 

97 raise ValueError("Humidity must be between 0 and 100") 

98 

99 if leaf_ratio is not None and leaf_ratio <= 0: 

100 raise ValueError("Leaf ratio must be positive") 

101 

102 super().__init__( 

103 tea_type=tea_type.value, 

104 step=step.value, 

105 temperature=temperature, 

106 duration=duration, 

107 humidity=humidity, 

108 leaf_ratio=leaf_ratio, 

109 quality=quality, 

110 ) 

111 

112 def __repr__(self) -> str: 

113 return ( 

114 f"TeaProcess({self['tea_type']}, {self['step']}, " 

115 f"temp={self['temperature']}°C, " 

116 f"duration={self['duration']}h)" 

117 ) 

118 

119 

120def create_tea_sequence_rule(tea_type: TeaType) -> DSLRule: 

121 """ 

122 Creates a rule enforcing the correct processing sequence for a tea type. 

123 

124 Example: 

125 green_tea_sequence = create_tea_sequence_rule(TeaType.GREEN) 

126 """ 

127 sequences = { 

128 TeaType.GREEN: [ 

129 ProcessingStep.PLUCKING, 

130 ProcessingStep.WITHERING, 

131 ProcessingStep.FIXING, 

132 ProcessingStep.ROLLING, 

133 ProcessingStep.DRYING, 

134 ], 

135 TeaType.OOLONG: [ 

136 ProcessingStep.PLUCKING, 

137 ProcessingStep.WITHERING, 

138 ProcessingStep.BRUISING, 

139 ProcessingStep.OXIDATION, 

140 ProcessingStep.FIXING, 

141 ProcessingStep.ROLLING, 

142 ProcessingStep.DRYING, 

143 ], 

144 TeaType.BLACK: [ 

145 ProcessingStep.PLUCKING, 

146 ProcessingStep.WITHERING, 

147 ProcessingStep.ROLLING, 

148 ProcessingStep.OXIDATION, 

149 ProcessingStep.DRYING, 

150 ], 

151 TeaType.PUERH: [ 

152 ProcessingStep.PLUCKING, 

153 ProcessingStep.WITHERING, 

154 ProcessingStep.FIXING, 

155 ProcessingStep.ROLLING, 

156 ProcessingStep.FERMENTATION, 

157 ProcessingStep.DRYING, 

158 ProcessingStep.AGING, 

159 ], 

160 } 

161 

162 required_steps = sequences.get(tea_type, []) 

163 

164 def check_sequence(seq: Sequence) -> bool: 

165 if not seq: 

166 return True 

167 

168 # Check tea type consistency 

169 if not all(step["tea_type"] == tea_type.value for step in seq): 

170 return False 

171 

172 # Extract steps in sequence 

173 steps = [ProcessingStep(step["step"]) for step in seq] 

174 

175 # Check if steps appear in correct order 

176 current_idx = 0 

177 for step in steps: 

178 if step not in required_steps: 

179 return False 

180 step_idx = required_steps.index(step) 

181 if step_idx < current_idx: 

182 return False 

183 current_idx = step_idx 

184 return True 

185 

186 return DSLRule(check_sequence, f"follows {tea_type.value} tea processing sequence") 

187 

188 

189def create_temperature_rule( 

190 step: ProcessingStep, min_temp: float, max_temp: float 

191) -> DSLRule: 

192 """ 

193 Creates a rule enforcing temperature range for a processing step. 

194 

195 Example: 

196 fixing_temp = create_temperature_rule(ProcessingStep.FIXING, 120, 140) 

197 """ 

198 

199 def check_temperature(seq: Sequence) -> bool: 

200 for process in seq: 

201 if ( 

202 ProcessingStep(process["step"]) == step 

203 and process["temperature"] is not None 

204 ): 

205 temp = process["temperature"] 

206 if not (min_temp <= temp <= max_temp): 

207 return False 

208 return True 

209 

210 return DSLRule( 

211 check_temperature, 

212 f"{step.value} temperature between {min_temp}°C and {max_temp}°C", 

213 ) 

214 

215 

216def create_humidity_rule( 

217 step: ProcessingStep, min_humidity: float, max_humidity: float 

218) -> DSLRule: 

219 """ 

220 Creates a rule enforcing humidity range for a processing step. 

221 

222 Example: 

223 withering_humidity = create_humidity_rule(ProcessingStep.WITHERING, 65, 75) 

224 """ 

225 

226 def check_humidity(seq: Sequence) -> bool: 

227 for process in seq: 

228 if ( 

229 ProcessingStep(process["step"]) == step 

230 and process["humidity"] is not None 

231 ): 

232 humidity = process["humidity"] 

233 if not (min_humidity <= humidity <= max_humidity): 

234 return False 

235 return True 

236 

237 return DSLRule( 

238 check_humidity, 

239 f"{step.value} humidity between {min_humidity}% and {max_humidity}%", 

240 ) 

241 

242 

243def create_duration_rule( 

244 step: ProcessingStep, min_hours: float, max_hours: float 

245) -> DSLRule: 

246 """ 

247 Creates a rule enforcing duration range for a processing step. 

248 

249 Example: 

250 oxidation_time = create_duration_rule(ProcessingStep.OXIDATION, 2, 4) 

251 """ 

252 

253 def check_duration(seq: Sequence) -> bool: 

254 for process in seq: 

255 if ( 

256 ProcessingStep(process["step"]) == step 

257 and process["duration"] is not None 

258 ): 

259 duration = process["duration"] 

260 if not (min_hours <= duration <= max_hours): 

261 return False 

262 return True 

263 

264 return DSLRule( 

265 check_duration, f"{step.value} duration between {min_hours}h and {max_hours}h" 

266 ) 

267 

268 

269def create_oxidation_level_rule(tea_type: TeaType) -> DSLRule: 

270 """ 

271 Creates a rule enforcing proper oxidation level for a tea type. 

272 

273 Example: 

274 oolong_oxidation = create_oxidation_level_rule(TeaType.OOLONG) 

275 """ 

276 # Oxidation levels in hours 

277 oxidation_times = { 

278 TeaType.GREEN: 0, 

279 TeaType.WHITE: 0, 

280 TeaType.YELLOW: 0.75, 

281 TeaType.OOLONG: 3, 

282 TeaType.BLACK: 5, 

283 } 

284 

285 target_time = oxidation_times.get(tea_type, 0) 

286 

287 def check_oxidation(seq: Sequence) -> bool: 

288 has_oxidation = False 

289 for process in seq: 

290 if ( 

291 ProcessingStep(process["step"]) == ProcessingStep.OXIDATION 

292 and process["duration"] is not None 

293 ): 

294 has_oxidation = True 

295 if abs(process["duration"] - target_time) > 0.5: 

296 return False 

297 return not has_oxidation if target_time == 0 else has_oxidation 

298 

299 return DSLRule(check_oxidation, f"{tea_type.value} tea oxidation level") 

300 

301 

302def create_quality_rule(min_metrics: QualityMetrics) -> DSLRule: 

303 """ 

304 Creates a rule enforcing minimum quality metrics. 

305 

306 Example: 

307 quality_standard = create_quality_rule(min_metrics) 

308 """ 

309 

310 def check_quality(seq: Sequence) -> bool: 

311 for process in seq: 

312 if process["quality"] is not None: 

313 quality = process["quality"] 

314 if ( 

315 quality.moisture_content > min_metrics.moisture_content 

316 or quality.leaf_integrity < min_metrics.leaf_integrity 

317 or quality.aroma_intensity < min_metrics.aroma_intensity 

318 ): 

319 return False 

320 return True 

321 

322 return DSLRule(check_quality, "meets minimum quality standards") 

323 

324 

325# Common processing rules 

326green_tea_rules = [ 

327 create_tea_sequence_rule(TeaType.GREEN), 

328 create_temperature_rule(ProcessingStep.FIXING, 120, 140), 

329 create_humidity_rule(ProcessingStep.WITHERING, 65, 75), 

330 create_duration_rule(ProcessingStep.DRYING, 0.5, 1.5), 

331] 

332 

333oolong_tea_rules = [ 

334 create_tea_sequence_rule(TeaType.OOLONG), 

335 create_temperature_rule(ProcessingStep.OXIDATION, 25, 30), 

336 create_humidity_rule(ProcessingStep.OXIDATION, 75, 85), 

337 create_oxidation_level_rule(TeaType.OOLONG), 

338] 

339 

340puerh_tea_rules = [ 

341 create_tea_sequence_rule(TeaType.PUERH), 

342 create_temperature_rule(ProcessingStep.FERMENTATION, 25, 35), 

343 create_humidity_rule(ProcessingStep.FERMENTATION, 85, 95), 

344 create_duration_rule(ProcessingStep.AGING, 720, float("inf")), # Minimum 30 days 

345] 

346 

347# Example sequences 

348green_tea_sequence = [ 

349 TeaProcess(TeaType.GREEN, ProcessingStep.PLUCKING), 

350 TeaProcess( 

351 TeaType.GREEN, ProcessingStep.WITHERING, temperature=25, duration=2, humidity=70 

352 ), 

353 TeaProcess( 

354 TeaType.GREEN, ProcessingStep.FIXING, temperature=130, duration=0.1 

355 ), # 6 minutes 

356 TeaProcess(TeaType.GREEN, ProcessingStep.ROLLING, duration=0.25), # 15 minutes 

357 TeaProcess(TeaType.GREEN, ProcessingStep.DRYING, temperature=80, duration=1), 

358] 

359 

360oolong_tea_sequence = [ 

361 TeaProcess(TeaType.OOLONG, ProcessingStep.PLUCKING), 

362 TeaProcess( 

363 TeaType.OOLONG, 

364 ProcessingStep.WITHERING, 

365 temperature=25, 

366 duration=2, 

367 humidity=70, 

368 ), 

369 TeaProcess(TeaType.OOLONG, ProcessingStep.BRUISING, duration=0.5), 

370 TeaProcess( 

371 TeaType.OOLONG, 

372 ProcessingStep.OXIDATION, 

373 temperature=28, 

374 duration=3, 

375 humidity=80, 

376 ), 

377 TeaProcess(TeaType.OOLONG, ProcessingStep.FIXING, temperature=130, duration=0.1), 

378 TeaProcess(TeaType.OOLONG, ProcessingStep.ROLLING, duration=0.25), 

379 TeaProcess(TeaType.OOLONG, ProcessingStep.DRYING, temperature=85, duration=1), 

380]