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
« prev ^ index » next coverage.py v7.6.12, created at 2025-02-27 10:39 -0600
1"""
2Tea Processing Rules.
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
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"""
20from dataclasses import dataclass
21from enum import Enum
22from typing import Dict, Optional
24from ..core import AbstractObject, Sequence
25from ..dsl import DSLRule
28class TeaType(Enum):
29 """Types of tea based on processing method."""
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)
40class ProcessingStep(Enum):
41 """Steps in tea processing."""
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
54@dataclass
55class QualityMetrics:
56 """Quality control metrics for tea processing."""
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
65class TeaProcess(AbstractObject):
66 """
67 A tea processing step with properties.
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 """
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")
93 if duration is not None and duration < 0:
94 raise ValueError("Duration cannot be negative")
96 if humidity is not None and not (0 <= humidity <= 100):
97 raise ValueError("Humidity must be between 0 and 100")
99 if leaf_ratio is not None and leaf_ratio <= 0:
100 raise ValueError("Leaf ratio must be positive")
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 )
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 )
120def create_tea_sequence_rule(tea_type: TeaType) -> DSLRule:
121 """
122 Creates a rule enforcing the correct processing sequence for a tea type.
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 }
162 required_steps = sequences.get(tea_type, [])
164 def check_sequence(seq: Sequence) -> bool:
165 if not seq:
166 return True
168 # Check tea type consistency
169 if not all(step["tea_type"] == tea_type.value for step in seq):
170 return False
172 # Extract steps in sequence
173 steps = [ProcessingStep(step["step"]) for step in seq]
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
186 return DSLRule(check_sequence, f"follows {tea_type.value} tea processing sequence")
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.
195 Example:
196 fixing_temp = create_temperature_rule(ProcessingStep.FIXING, 120, 140)
197 """
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
210 return DSLRule(
211 check_temperature,
212 f"{step.value} temperature between {min_temp}°C and {max_temp}°C",
213 )
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.
222 Example:
223 withering_humidity = create_humidity_rule(ProcessingStep.WITHERING, 65, 75)
224 """
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
237 return DSLRule(
238 check_humidity,
239 f"{step.value} humidity between {min_humidity}% and {max_humidity}%",
240 )
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.
249 Example:
250 oxidation_time = create_duration_rule(ProcessingStep.OXIDATION, 2, 4)
251 """
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
264 return DSLRule(
265 check_duration, f"{step.value} duration between {min_hours}h and {max_hours}h"
266 )
269def create_oxidation_level_rule(tea_type: TeaType) -> DSLRule:
270 """
271 Creates a rule enforcing proper oxidation level for a tea type.
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 }
285 target_time = oxidation_times.get(tea_type, 0)
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
299 return DSLRule(check_oxidation, f"{tea_type.value} tea oxidation level")
302def create_quality_rule(min_metrics: QualityMetrics) -> DSLRule:
303 """
304 Creates a rule enforcing minimum quality metrics.
306 Example:
307 quality_standard = create_quality_rule(min_metrics)
308 """
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
322 return DSLRule(check_quality, "meets minimum quality standards")
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]
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]
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]
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]
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]