Skip to content

Commit 645a5b5

Browse files
Merge pull request #178 from worldcoin/dev
Fix Error Handling and None Value Processing in Templates Aggregation Pipeline (#177)
2 parents cd7ac1d + c52b944 commit 645a5b5

12 files changed

Lines changed: 454 additions & 30 deletions

colab/ConfiguringCustomPipeline.ipynb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@
126126
"```yaml\n",
127127
"metadata:\n",
128128
" pipeline_name: iris_pipeline\n",
129-
" iris_version: 1.9.6\n",
129+
" iris_version: 1.9.7\n",
130130
"```\n",
131131
"\n",
132132
"The top YAML file contains `IRISPipeline` metadata, used to both describe `IRISPipeline` and specify package parameters that are later used to verify compatibility between `iris` package version/release and later, specified in the `pipeline` YAML file section, pipeline's graph.\n",
@@ -206,7 +206,7 @@
206206
"outputs": [],
207207
"source": [
208208
"default_pipeline_conf = {\n",
209-
" \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.9.6\"},\n",
209+
" \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.9.7\"},\n",
210210
" \"pipeline\": [\n",
211211
" {\n",
212212
" \"name\": \"segmentation\",\n",
@@ -481,7 +481,7 @@
481481
"outputs": [],
482482
"source": [
483483
"new_pipeline_conf = {\n",
484-
" \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.9.6\"},\n",
484+
" \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.9.7\"},\n",
485485
" \"pipeline\": [\n",
486486
" {\n",
487487
" \"name\": \"segmentation\",\n",
@@ -760,7 +760,7 @@
760760
"outputs": [],
761761
"source": [
762762
"default_pipeline_conf = {\n",
763-
" \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.9.6\"},\n",
763+
" \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.9.7\"},\n",
764764
" \"pipeline\": [\n",
765765
" {\n",
766766
" \"name\": \"segmentation\",\n",
@@ -1035,7 +1035,7 @@
10351035
"outputs": [],
10361036
"source": [
10371037
"new_pipeline_conf = {\n",
1038-
" \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.9.6\"},\n",
1038+
" \"metadata\": {\"pipeline_name\": \"iris_pipeline\", \"iris_version\": \"1.9.7\"},\n",
10391039
" \"pipeline\": [\n",
10401040
" {\n",
10411041
" \"name\": \"segmentation\",\n",

docs/source/examples/custom_pipeline.rst

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ When the ``IRISPipeline`` pipeline is created with default parameters, it's grap
1818
1919
metadata:
2020
pipeline_name: iris_pipeline
21-
iris_version: 1.9.6
21+
iris_version: 1.9.7
2222
2323
The top YAML file contains ``IRISPipeline`` metadata, used to both describe ``IRISPipeline`` and specify package parameters that are later used to verify compatibility between ``iris`` package version/release and later, specified in the ``pipeline`` YAML file section, pipeline's graph.
2424

@@ -93,7 +93,7 @@ First let's intantiate ``IRISPipeline`` with default configuration and see ``iri
9393
.. code-block:: python
9494
9595
default_pipeline_conf = {
96-
"metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.9.6"},
96+
"metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.9.7"},
9797
"pipeline": [
9898
{
9999
"name": "segmentation",
@@ -320,7 +320,7 @@ As expected all threshold values are set to default ``0.5`` value. Now, let's mo
320320
.. code-block:: python
321321
322322
new_pipeline_conf = {
323-
"metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.9.6"},
323+
"metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.9.7"},
324324
"pipeline": [
325325
{
326326
"name": "segmentation",
@@ -552,7 +552,7 @@ First let's instantiate ``IRISPipeline`` with default configuration and see node
552552
.. code-block:: python
553553
554554
default_pipeline_conf = {
555-
"metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.9.6"},
555+
"metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.9.7"},
556556
"pipeline": [
557557
{
558558
"name": "segmentation",
@@ -783,7 +783,7 @@ As expected, ``input_polygons`` argument of the ``run`` method is taken from the
783783
.. code-block:: python
784784
785785
new_pipeline_conf = {
786-
"metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.9.6"},
786+
"metadata": {"pipeline_name": "iris_pipeline", "iris_version": "1.9.7"},
787787
"pipeline": [
788788
{
789789
"name": "segmentation",

src/iris/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.9.6"
1+
__version__ = "1.9.7"

src/iris/io/errors.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,9 @@ class IdentityValidationError(Exception):
158158
"""Identity validation Error class."""
159159

160160
pass
161+
162+
163+
class DifferentImageIdsTemplatesListLenError(Exception):
164+
"""Error raised when the number of image_ids doesn't match the number of templates."""
165+
166+
pass

src/iris/orchestration/output_builders.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,9 @@ def __get_multiframe_iris_pipeline_metadata(call_trace: PipelineCallTraceStorage
267267
),
268268
OutputFieldSpec(
269269
key="templates_aggregation_metadata",
270-
extractor=lambda ct: {k: v for k, v in ct.get("aggregation_result", {}).items() if k != "iris_template"},
270+
extractor=lambda ct: None
271+
if (agg := ct.get("aggregation_result")) is None
272+
else {k: v for k, v in agg.items() if k != "iris_template"},
271273
safe_serialize=False,
272274
),
273275
]
@@ -292,7 +294,9 @@ def __get_multiframe_iris_pipeline_metadata(call_trace: PipelineCallTraceStorage
292294
),
293295
OutputFieldSpec(
294296
key="templates_aggregation_metadata",
295-
extractor=lambda ct: {k: v for k, v in ct.get("aggregation_result", {}).items() if k != "iris_template"},
297+
extractor=lambda ct: None
298+
if (agg := ct.get("aggregation_result")) is None
299+
else {k: v for k, v in agg.items() if k != "iris_template"},
296300
safe_serialize=False,
297301
),
298302
]

src/iris/pipelines/confs/multiframe_iris_pipeline.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
metadata:
22
pipeline_name: multiframe_iris_pipeline
3-
iris_version: 1.9.6
3+
iris_version: 1.9.7
44

55
# Configuration for individual image processing (IRISPipeline)
66
iris_pipeline:
77
metadata:
88
pipeline_name: iris_pipeline
9-
iris_version: 1.9.6
9+
iris_version: 1.9.7
1010

1111
pipeline:
1212
- name: segmentation
@@ -315,7 +315,7 @@ iris_pipeline:
315315
# Configuration for template aggregation (TemplatesAggregationPipeline)
316316
templates_aggregation_pipeline:
317317
metadata:
318-
iris_version: 1.9.6
318+
iris_version: 1.9.7
319319
pipeline_name: templates_aggregation
320320

321321
pipeline:

src/iris/pipelines/confs/pipeline.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
metadata:
22
pipeline_name: iris_pipeline
3-
iris_version: 1.9.6
3+
iris_version: 1.9.7
44

55
pipeline:
66
- name: segmentation

src/iris/pipelines/confs/templates_aggregation_pipeline.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
metadata:
22
pipeline_name: iris_pipeline
3-
iris_version: 1.9.6
3+
iris_version: 1.9.7
44

55
pipeline:
66
- name: segmentation
@@ -307,7 +307,7 @@ pipeline:
307307

308308
templates_aggregation:
309309
metadata:
310-
iris_version: 1.9.6
310+
iris_version: 1.9.7
311311
pipeline_name: templates_aggregation
312312

313313
pipeline:

src/iris/pipelines/templates_aggregation_pipeline.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from iris.callbacks.pipeline_trace import PipelineCallTraceStorage
99
from iris.io.class_configs import Algorithm
1010
from iris.io.dataclasses import IrisTemplate, IrisTemplateWithId
11+
from iris.io.errors import DifferentImageIdsTemplatesListLenError
1112
from iris.orchestration.environment import Environment
1213
from iris.orchestration.error_managers import store_error_manager
1314
from iris.orchestration.output_builders import (
@@ -67,17 +68,21 @@ def __init__(
6768
def run(
6869
self, templates: List[IrisTemplate], image_ids: Optional[List[str]] = None, *args: Any, **kwargs: Any
6970
) -> Any:
70-
# Validate input consistency
71-
if image_ids is not None and len(image_ids) != len(templates):
72-
raise ValueError(
73-
f"Number of image_ids ({len(image_ids)}) must match number of templates ({len(templates)})"
74-
)
75-
76-
# Create IrisTemplateWithId
77-
templates_with_ids = []
78-
for i, template in enumerate(templates):
79-
image_id = image_ids[i] if image_ids else f"frame_{i}"
80-
templates_with_ids.append(IrisTemplateWithId.from_template(template, image_id))
71+
try:
72+
# Validate input consistency
73+
if image_ids is not None and len(image_ids) != len(templates):
74+
raise DifferentImageIdsTemplatesListLenError(
75+
f"Number of image_ids ({len(image_ids)}) must match number of templates ({len(templates)})"
76+
)
77+
78+
# Create IrisTemplateWithId
79+
templates_with_ids = []
80+
for i, template in enumerate(templates):
81+
image_id = image_ids[i] if image_ids else f"frame_{i}"
82+
templates_with_ids.append(IrisTemplateWithId.from_template(template, image_id))
83+
except Exception as e:
84+
self.env.error_manager(self.call_trace, e)
85+
return self._handle_output(*args, **kwargs)
8186

8287
pipeline_input = {"templates_with_ids": templates_with_ids}
8388
return super().run(pipeline_input, *args, **kwargs)

tests/e2e_tests/pipelines/test_e2e_multiframe_aggregation_pipeline.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,3 +508,136 @@ def test_pipeline_reproducibility(self, standalone_aggregation_config):
508508
np.testing.assert_array_equal(
509509
result1["iris_template"].mask_codes[i], result2["iris_template"].mask_codes[i]
510510
)
511+
512+
513+
class TestE2ETemplatesAggregationFailures:
514+
"""End-to-end tests for templates aggregation pipeline failure scenarios."""
515+
516+
@pytest.fixture
517+
def failure_prone_config_no_clusters(self):
518+
"""Configuration that will cause no identity clusters to be found."""
519+
return {
520+
"metadata": {"pipeline_name": "templates_aggregation", "iris_version": "1.7.1"},
521+
"pipeline": [
522+
{
523+
"name": "templates_alignment",
524+
"algorithm": {
525+
"class_name": "iris.nodes.templates_alignment.hamming_distance_based.HammingDistanceBasedAlignment",
526+
"params": {
527+
"rotation_shift": 15,
528+
"use_first_as_reference": False,
529+
"normalise": True,
530+
"reference_selection_method": "linear",
531+
},
532+
},
533+
"inputs": [{"name": "templates_with_ids", "source_node": "input"}],
534+
"callbacks": [],
535+
},
536+
{
537+
"name": "identity_validation",
538+
"algorithm": {
539+
"class_name": "iris.nodes.templates_filter.single_identity_filter.TemplateIdentityFilter",
540+
"params": {
541+
"identity_distance_threshold": 0.05, # Very low threshold
542+
"identity_validation_action": "raise_error", # Will raise error
543+
"min_templates_after_validation": 1,
544+
},
545+
},
546+
"inputs": [{"name": "aligned_templates", "source_node": "templates_alignment"}],
547+
"callbacks": [],
548+
},
549+
{
550+
"name": "templates_aggregation",
551+
"algorithm": {
552+
"class_name": "iris.nodes.templates_aggregation.majority_vote.MajorityVoteAggregation",
553+
"params": {
554+
"consistency_threshold": 0.75,
555+
"mask_threshold": 0.01,
556+
"use_inconsistent_bits": True,
557+
"inconsistent_bit_threshold": 0.4,
558+
},
559+
},
560+
"inputs": [{"name": "templates", "source_node": "identity_validation"}],
561+
"callbacks": [],
562+
},
563+
],
564+
}
565+
566+
@pytest.fixture
567+
def very_different_templates(self):
568+
"""Create templates that are very different from each other (high Hamming distances)."""
569+
templates = []
570+
571+
for i in range(4):
572+
iris_codes = []
573+
mask_codes = []
574+
575+
for wavelet in range(2):
576+
# Create very different patterns for each template
577+
if i == 0:
578+
# Template 0: mostly zeros
579+
iris_code = np.zeros((8, 32, 2), dtype=bool)
580+
elif i == 1:
581+
# Template 1: mostly ones
582+
iris_code = np.ones((8, 32, 2), dtype=bool)
583+
elif i == 2:
584+
# Template 2: checkerboard pattern
585+
iris_code = np.zeros((8, 32, 2), dtype=bool)
586+
iris_code[::2, ::2] = True
587+
iris_code[1::2, 1::2] = True
588+
else:
589+
# Template 3: inverse checkerboard
590+
iris_code = np.ones((8, 32, 2), dtype=bool)
591+
iris_code[::2, ::2] = False
592+
iris_code[1::2, 1::2] = False
593+
594+
# All templates have good masks
595+
mask_code = np.ones((8, 32, 2), dtype=bool)
596+
597+
iris_codes.append(iris_code)
598+
mask_codes.append(mask_code)
599+
600+
template = IrisTemplate(iris_codes=iris_codes, mask_codes=mask_codes, iris_code_version="v2.1")
601+
templates.append(template)
602+
603+
return templates
604+
605+
def test_e2e_no_identity_clusters_found(self, failure_prone_config_no_clusters, very_different_templates):
606+
"""Test end-to-end pipeline when no identity clusters are found."""
607+
env = Environment(
608+
pipeline_output_builder=build_simple_templates_aggregation_output,
609+
error_manager=store_error_manager,
610+
call_trace_initialiser=PipelineCallTraceStorage.initialise,
611+
)
612+
613+
pipeline = TemplatesAggregationPipeline(config=failure_prone_config_no_clusters, env=env, subconfig_key="")
614+
615+
image_ids = [f"image_{i}" for i in range(len(very_different_templates))]
616+
result = pipeline.run(very_different_templates, image_ids)
617+
618+
# Should have error due to no clusters found
619+
assert result["error"] is not None
620+
assert "no identity clusters" in result["error"]["message"]
621+
assert result["iris_template"] is None
622+
623+
# Metadata should still be generated
624+
assert result["metadata"] is not None
625+
assert result["metadata"]["input_templates_count"] == len(very_different_templates)
626+
627+
def test_e2e_empty_templates_list(self):
628+
"""Test end-to-end pipeline with empty templates list."""
629+
env = Environment(
630+
pipeline_output_builder=build_simple_templates_aggregation_output,
631+
error_manager=store_error_manager,
632+
call_trace_initialiser=PipelineCallTraceStorage.initialise,
633+
)
634+
635+
pipeline = TemplatesAggregationPipeline(env=env, subconfig_key="")
636+
637+
result = pipeline.run(templates=[], image_ids=["image1"])
638+
639+
assert result["error"] is not None
640+
assert "must match number of templates" in result["error"]["message"]
641+
assert result["iris_template"] is None
642+
assert result["metadata"] is not None
643+
assert result["metadata"]["input_templates_count"] is None

0 commit comments

Comments
 (0)