aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/argaze.test/PupilFeatures.py167
-rw-r--r--src/argaze.test/PupillAnalysis/WorkloadIndex.py (renamed from src/argaze.test/PupilAnalysis/WorkloadIndex.py)30
-rw-r--r--src/argaze.test/PupillAnalysis/__init__.py (renamed from src/argaze.test/PupilAnalysis/__init__.py)0
-rw-r--r--src/argaze.test/PupillFeatures.py167
-rw-r--r--src/argaze/PupilAnalysis/__init__.py4
-rw-r--r--src/argaze/PupilFeatures.py106
-rw-r--r--src/argaze/PupillAnalysis/WorkloadIndex.py (renamed from src/argaze/PupilAnalysis/WorkloadIndex.py)20
-rw-r--r--src/argaze/PupillAnalysis/__init__.py4
-rw-r--r--src/argaze/PupillFeatures.py106
-rw-r--r--src/argaze/__init__.py2
10 files changed, 303 insertions, 303 deletions
diff --git a/src/argaze.test/PupilFeatures.py b/src/argaze.test/PupilFeatures.py
deleted file mode 100644
index ecc94cd..0000000
--- a/src/argaze.test/PupilFeatures.py
+++ /dev/null
@@ -1,167 +0,0 @@
-#!/usr/bin/env python
-
-""" """
-
-__author__ = "Théo de la Hogue"
-__credits__ = []
-__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)"
-__license__ = "BSD"
-
-import unittest
-
-from argaze import PupilFeatures
-
-import numpy
-
-def random_pupil_diameters(size):
- """ Generate random TimeStampedPupilDiameters for testing purpose.
- Timestamps are current time.
- PupilDiameters are random values.
- """
-
- import random
- import time
-
- ts_pupil_diameters = PupilFeatures.TimeStampedPupilDiameters()
-
- for i in range(0, size):
-
- # Edit pupil diameter
- random_pupil_diameter = PupilFeatures.PupilDiameter(random.random())
-
- # Store pupil diameter
- ts_pupil_diameters[time.time()] = random_pupil_diameter
-
- return ts_pupil_diameters
-
-class TestPupilDiameterClass(unittest.TestCase):
- """Test PupilDiameter class."""
-
- def test_new(self):
- """Test PupilDiameter creation."""
-
- # Check empty PupilDiameter
- empty_pupil_diameter = PupilFeatures.PupilDiameter()
-
- self.assertEqual(empty_pupil_diameter.value, 0.)
- self.assertEqual(empty_pupil_diameter.valid, False)
-
- # Check float PupilDiameter
- float_pupil_diameter = PupilFeatures.PupilDiameter(1.23)
-
- self.assertEqual(float_pupil_diameter.value, 1.23)
- self.assertEqual(float_pupil_diameter.valid, True)
-
- def test_properties(self):
- """Test PupilDiameter properties cannot be modified after creation."""
-
- pupil_diameter = PupilFeatures.PupilDiameter()
-
- # Check that pupil diameter value setting fails
- with self.assertRaises(AttributeError):
-
- pupil_diameter.value = 123
-
- self.assertNotEqual(pupil_diameter.value, 123)
- self.assertEqual(pupil_diameter.value, 0.)
-
- def test___repr__(self):
- """Test PupilDiameter string representation."""
-
- # Check empty PupilDiameter representation
- self.assertEqual(repr(PupilFeatures.PupilDiameter()), "{\"value\": 0.0}")
-
-class TestUnvalidPupilDiameterClass(unittest.TestCase):
- """Test UnvalidPupilDiameter class."""
-
- def test_new(self):
- """Test UnvalidPupilDiameter creation."""
-
- unvalid_pupil_diameter = PupilFeatures.UnvalidPupilDiameter()
-
- self.assertEqual(unvalid_pupil_diameter.value, 0.)
- self.assertEqual(unvalid_pupil_diameter.valid, False)
-
- def test___repr__(self):
- """Test UnvalidPupilDiameter string representation."""
-
- self.assertEqual(repr(PupilFeatures.UnvalidPupilDiameter()), "{\"message\": null, \"value\": 0.0}")
-
-class TestTimeStampedPupilDiametersClass(unittest.TestCase):
- """Test TimeStampedPupilDiameters class."""
-
- def test___setitem__(self):
- """Test __setitem__ method."""
-
- ts_pupil_diameters = PupilFeatures.TimeStampedPupilDiameters()
- ts_pupil_diameters[0] = PupilFeatures.PupilDiameter()
- ts_pupil_diameters[1] = PupilFeatures.UnvalidPupilDiameter()
- ts_pupil_diameters[2] = {"value": 1.23}
-
- # Check PupilDiameter is correctly stored and accessible as a PupilDiameter
- self.assertIsInstance(ts_pupil_diameters[0], PupilFeatures.PupilDiameter)
- self.assertEqual(ts_pupil_diameters[0].valid, False)
-
- # Check UnvalidPupilDiameter is correctly stored and accessible as a UnvalidPupilDiameter
- self.assertIsInstance(ts_pupil_diameters[1], PupilFeatures.UnvalidPupilDiameter)
- self.assertEqual(ts_pupil_diameters[1].valid, False)
-
- # Check dict with "value" and "precision" keys is correctly stored and accessible as a PupilDiameter
- self.assertIsInstance(ts_pupil_diameters[2], PupilFeatures.PupilDiameter)
- self.assertEqual(ts_pupil_diameters[2].valid, True)
-
- # Check that bad data type insertion fails
- with self.assertRaises(AssertionError):
-
- ts_pupil_diameters[3] = "This string is not a pupil diameter value."
-
- # Check that dict with bad keys insertion fails
- with self.assertRaises(AssertionError):
-
- ts_pupil_diameters[4] = {"bad_key": 0.}
-
- def test___repr__(self):
- """Test inherited string representation."""
-
- ts_pupil_diameters = PupilFeatures.TimeStampedPupilDiameters()
-
- self.assertEqual(repr(PupilFeatures.TimeStampedPupilDiameters()), "{}")
-
- ts_pupil_diameters[0] = PupilFeatures.PupilDiameter()
-
- self.assertEqual(repr(ts_pupil_diameters), "{\"0\": {\"value\": 0.0}}")
-
- ts_pupil_diameters[0] = PupilFeatures.UnvalidPupilDiameter()
-
- self.assertEqual(repr(ts_pupil_diameters), "{\"0\": {\"message\": null, \"value\": 0.0}}")
-
- def test_as_dataframe(self):
- """Test inherited as_dataframe method."""
-
- ts_pupil_diameters_dataframe = random_pupil_diameters(10).as_dataframe()
-
- # Check dataframe conversion
- self.assertEqual(ts_pupil_diameters_dataframe.index.name, "timestamp")
- self.assertEqual(ts_pupil_diameters_dataframe.index.size, 10)
-
- self.assertEqual(ts_pupil_diameters_dataframe.columns.size, 1)
- self.assertEqual(ts_pupil_diameters_dataframe.columns[0], "value")
-
- self.assertEqual(ts_pupil_diameters_dataframe["value"].dtype, 'float64')
-
- # Check unvalid diameter conversion
- ts_pupil_diameters = PupilFeatures.TimeStampedPupilDiameters()
- ts_pupil_diameters[0] = PupilFeatures.UnvalidPupilDiameter()
- ts_pupil_diameters_dataframe = ts_pupil_diameters.as_dataframe()
-
- self.assertEqual(ts_pupil_diameters_dataframe.index.name, "timestamp")
- self.assertEqual(ts_pupil_diameters_dataframe.index.size, 1)
-
- self.assertEqual(ts_pupil_diameters_dataframe.columns.size, 1)
- self.assertEqual(ts_pupil_diameters_dataframe.columns[0], "value")
-
- self.assertEqual(ts_pupil_diameters_dataframe["value"].dtype, 'float64')
-
-if __name__ == '__main__':
-
- unittest.main() \ No newline at end of file
diff --git a/src/argaze.test/PupilAnalysis/WorkloadIndex.py b/src/argaze.test/PupillAnalysis/WorkloadIndex.py
index fec32ef..d60b6ae 100644
--- a/src/argaze.test/PupilAnalysis/WorkloadIndex.py
+++ b/src/argaze.test/PupillAnalysis/WorkloadIndex.py
@@ -10,8 +10,8 @@ __license__ = "BSD"
import unittest
import math
-from argaze import PupilFeatures
-from argaze.PupilAnalysis import WorkloadIndex
+from argaze import PupillFeatures
+from argaze.PupillAnalysis import WorkloadIndex
class TestWorkloadIndexClass(unittest.TestCase):
"""Test WorkloadIndex class."""
@@ -19,21 +19,21 @@ class TestWorkloadIndexClass(unittest.TestCase):
def test_analysis(self):
"""Test WorkloadIndex analysis."""
- ts_pupil_diameters = {
- 0: PupilFeatures.PupilDiameter(1.),
- 1: PupilFeatures.PupilDiameter(1.1),
- 2: PupilFeatures.PupilDiameter(1.2),
- 3: PupilFeatures.PupilDiameter(1.3),
- 4: PupilFeatures.PupilDiameter(1.2),
- 5: PupilFeatures.PupilDiameter(1.1),
- 6: PupilFeatures.PupilDiameter(1.),
- 7: PupilFeatures.PupilDiameter(0.9),
- 8: PupilFeatures.PupilDiameter(0.8),
- 9: PupilFeatures.PupilDiameter(0.7)
+ ts_pupill_diameters = {
+ 0: PupillFeatures.PupillDiameter(1.),
+ 1: PupillFeatures.PupillDiameter(1.1),
+ 2: PupillFeatures.PupillDiameter(1.2),
+ 3: PupillFeatures.PupillDiameter(1.3),
+ 4: PupillFeatures.PupillDiameter(1.2),
+ 5: PupillFeatures.PupillDiameter(1.1),
+ 6: PupillFeatures.PupillDiameter(1.),
+ 7: PupillFeatures.PupillDiameter(0.9),
+ 8: PupillFeatures.PupillDiameter(0.8),
+ 9: PupillFeatures.PupillDiameter(0.7)
}
- pupil_diameter_analyzer = WorkloadIndex.PupilDiameterAnalyzer(reference=PupilFeatures.PupilDiameter(1.), period=3)
- ts_analysis = pupil_diameter_analyzer.browse(PupilFeatures.TimeStampedPupilDiameters(ts_pupil_diameters))
+ pupill_diameter_analyzer = WorkloadIndex.PupillDiameterAnalyzer(reference=PupillFeatures.PupillDiameter(1.), period=3)
+ ts_analysis = pupill_diameter_analyzer.browse(PupillFeatures.TimeStampedPupillDiameters(ts_pupill_diameters))
# Check result size
self.assertEqual(len(ts_analysis), 3)
diff --git a/src/argaze.test/PupilAnalysis/__init__.py b/src/argaze.test/PupillAnalysis/__init__.py
index e69de29..e69de29 100644
--- a/src/argaze.test/PupilAnalysis/__init__.py
+++ b/src/argaze.test/PupillAnalysis/__init__.py
diff --git a/src/argaze.test/PupillFeatures.py b/src/argaze.test/PupillFeatures.py
new file mode 100644
index 0000000..f0e8e1b
--- /dev/null
+++ b/src/argaze.test/PupillFeatures.py
@@ -0,0 +1,167 @@
+#!/usr/bin/env python
+
+""" """
+
+__author__ = "Théo de la Hogue"
+__credits__ = []
+__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)"
+__license__ = "BSD"
+
+import unittest
+
+from argaze import PupillFeatures
+
+import numpy
+
+def random_pupill_diameters(size):
+ """ Generate random TimeStampedPupillDiameters for testing purpose.
+ Timestamps are current time.
+ PupillDiameters are random values.
+ """
+
+ import random
+ import time
+
+ ts_pupill_diameters = PupillFeatures.TimeStampedPupillDiameters()
+
+ for i in range(0, size):
+
+ # Edit pupill diameter
+ random_pupill_diameter = PupillFeatures.PupillDiameter(random.random())
+
+ # Store pupill diameter
+ ts_pupill_diameters[time.time()] = random_pupill_diameter
+
+ return ts_pupill_diameters
+
+class TestPupillDiameterClass(unittest.TestCase):
+ """Test PupillDiameter class."""
+
+ def test_new(self):
+ """Test PupillDiameter creation."""
+
+ # Check empty PupillDiameter
+ empty_pupill_diameter = PupillFeatures.PupillDiameter()
+
+ self.assertEqual(empty_pupill_diameter.value, 0.)
+ self.assertEqual(empty_pupill_diameter.valid, False)
+
+ # Check float PupillDiameter
+ float_pupill_diameter = PupillFeatures.PupillDiameter(1.23)
+
+ self.assertEqual(float_pupill_diameter.value, 1.23)
+ self.assertEqual(float_pupill_diameter.valid, True)
+
+ def test_properties(self):
+ """Test PupillDiameter properties cannot be modified after creation."""
+
+ pupill_diameter = PupillFeatures.PupillDiameter()
+
+ # Check that pupill diameter value setting fails
+ with self.assertRaises(AttributeError):
+
+ pupill_diameter.value = 123
+
+ self.assertNotEqual(pupill_diameter.value, 123)
+ self.assertEqual(pupill_diameter.value, 0.)
+
+ def test___repr__(self):
+ """Test PupillDiameter string representation."""
+
+ # Check empty PupillDiameter representation
+ self.assertEqual(repr(PupillFeatures.PupillDiameter()), "{\"value\": 0.0}")
+
+class TestUnvalidPupillDiameterClass(unittest.TestCase):
+ """Test UnvalidPupillDiameter class."""
+
+ def test_new(self):
+ """Test UnvalidPupillDiameter creation."""
+
+ unvalid_pupill_diameter = PupillFeatures.UnvalidPupillDiameter()
+
+ self.assertEqual(unvalid_pupill_diameter.value, 0.)
+ self.assertEqual(unvalid_pupill_diameter.valid, False)
+
+ def test___repr__(self):
+ """Test UnvalidPupillDiameter string representation."""
+
+ self.assertEqual(repr(PupillFeatures.UnvalidPupillDiameter()), "{\"message\": null, \"value\": 0.0}")
+
+class TestTimeStampedPupillDiametersClass(unittest.TestCase):
+ """Test TimeStampedPupillDiameters class."""
+
+ def test___setitem__(self):
+ """Test __setitem__ method."""
+
+ ts_pupill_diameters = PupillFeatures.TimeStampedPupillDiameters()
+ ts_pupill_diameters[0] = PupillFeatures.PupillDiameter()
+ ts_pupill_diameters[1] = PupillFeatures.UnvalidPupillDiameter()
+ ts_pupill_diameters[2] = {"value": 1.23}
+
+ # Check PupillDiameter is correctly stored and accessible as a PupillDiameter
+ self.assertIsInstance(ts_pupill_diameters[0], PupillFeatures.PupillDiameter)
+ self.assertEqual(ts_pupill_diameters[0].valid, False)
+
+ # Check UnvalidPupillDiameter is correctly stored and accessible as a UnvalidPupillDiameter
+ self.assertIsInstance(ts_pupill_diameters[1], PupillFeatures.UnvalidPupillDiameter)
+ self.assertEqual(ts_pupill_diameters[1].valid, False)
+
+ # Check dict with "value" and "precision" keys is correctly stored and accessible as a PupillDiameter
+ self.assertIsInstance(ts_pupill_diameters[2], PupillFeatures.PupillDiameter)
+ self.assertEqual(ts_pupill_diameters[2].valid, True)
+
+ # Check that bad data type insertion fails
+ with self.assertRaises(AssertionError):
+
+ ts_pupill_diameters[3] = "This string is not a pupill diameter value."
+
+ # Check that dict with bad keys insertion fails
+ with self.assertRaises(AssertionError):
+
+ ts_pupill_diameters[4] = {"bad_key": 0.}
+
+ def test___repr__(self):
+ """Test inherited string representation."""
+
+ ts_pupill_diameters = PupillFeatures.TimeStampedPupillDiameters()
+
+ self.assertEqual(repr(PupillFeatures.TimeStampedPupillDiameters()), "{}")
+
+ ts_pupill_diameters[0] = PupillFeatures.PupillDiameter()
+
+ self.assertEqual(repr(ts_pupill_diameters), "{\"0\": {\"value\": 0.0}}")
+
+ ts_pupill_diameters[0] = PupillFeatures.UnvalidPupillDiameter()
+
+ self.assertEqual(repr(ts_pupill_diameters), "{\"0\": {\"message\": null, \"value\": 0.0}}")
+
+ def test_as_dataframe(self):
+ """Test inherited as_dataframe method."""
+
+ ts_pupill_diameters_dataframe = random_pupill_diameters(10).as_dataframe()
+
+ # Check dataframe conversion
+ self.assertEqual(ts_pupill_diameters_dataframe.index.name, "timestamp")
+ self.assertEqual(ts_pupill_diameters_dataframe.index.size, 10)
+
+ self.assertEqual(ts_pupill_diameters_dataframe.columns.size, 1)
+ self.assertEqual(ts_pupill_diameters_dataframe.columns[0], "value")
+
+ self.assertEqual(ts_pupill_diameters_dataframe["value"].dtype, 'float64')
+
+ # Check unvalid diameter conversion
+ ts_pupill_diameters = PupillFeatures.TimeStampedPupillDiameters()
+ ts_pupill_diameters[0] = PupillFeatures.UnvalidPupillDiameter()
+ ts_pupill_diameters_dataframe = ts_pupill_diameters.as_dataframe()
+
+ self.assertEqual(ts_pupill_diameters_dataframe.index.name, "timestamp")
+ self.assertEqual(ts_pupill_diameters_dataframe.index.size, 1)
+
+ self.assertEqual(ts_pupill_diameters_dataframe.columns.size, 1)
+ self.assertEqual(ts_pupill_diameters_dataframe.columns[0], "value")
+
+ self.assertEqual(ts_pupill_diameters_dataframe["value"].dtype, 'float64')
+
+if __name__ == '__main__':
+
+ unittest.main() \ No newline at end of file
diff --git a/src/argaze/PupilAnalysis/__init__.py b/src/argaze/PupilAnalysis/__init__.py
deleted file mode 100644
index c563968..0000000
--- a/src/argaze/PupilAnalysis/__init__.py
+++ /dev/null
@@ -1,4 +0,0 @@
-"""
-Class interface to work with various pupil analysis algorithms.
-"""
-__all__ = ['WorkloadIndex'] \ No newline at end of file
diff --git a/src/argaze/PupilFeatures.py b/src/argaze/PupilFeatures.py
deleted file mode 100644
index 3ba9576..0000000
--- a/src/argaze/PupilFeatures.py
+++ /dev/null
@@ -1,106 +0,0 @@
-#!/usr/bin/env python
-
-""" """
-
-__author__ = "Théo de la Hogue"
-__credits__ = []
-__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)"
-__license__ = "BSD"
-
-from typing import TypeVar
-from dataclasses import dataclass, field
-import json
-
-from argaze import DataStructures
-
-@dataclass(frozen=True)
-class PupilDiameter():
- """Define pupil diameter as ..."""
-
- value: float = field(default=0.)
- """Pupil diameter value."""
-
- @property
- def valid(self) -> bool:
- """Is the value not 0"""
-
- return self.value != 0.
-
- def __repr__(self):
- """String representation"""
-
- return json.dumps(self, ensure_ascii = False, default=vars)
-
-class UnvalidPupilDiameter(PupilDiameter):
- """Unvalid pupil diameter."""
-
- def __init__(self, message=None):
-
- self.message = message
-
- super().__init__(0.)
-
-TimeStampedPupilDiametersType = TypeVar('TimeStampedPupilDiameters', bound="TimeStampedPupilDiameters")
-# Type definition for type annotation convenience
-
-class TimeStampedPupilDiameters(DataStructures.TimeStampedBuffer):
- """Define timestamped buffer to store pupil diameters."""
-
- def __setitem__(self, key, value: PupilDiameter|dict):
- """Force PupilDiameter storage."""
-
- # Convert dict into PupilDiameter
- if type(value) == dict:
-
- assert(set(['value']).issubset(value.keys()))
-
- if 'message' in value.keys():
-
- value = UnvalidPupilDiameter(value['message'])
-
- else:
-
- value = PupilDiameter(value['value'])
-
- assert(type(value) == PupilDiameter or type(value) == UnvalidPupilDiameter)
-
- super().__setitem__(key, value)
-
- @classmethod
- def from_json(self, json_filepath: str) -> TimeStampedPupilDiametersType:
- """Create a TimeStampedPupilDiametersType from .json file."""
-
- with open(json_filepath, encoding='utf-8') as ts_buffer_file:
-
- json_buffer = json.load(ts_buffer_file)
-
- return TimeStampedPupilDiameters({ast.literal_eval(ts_str): json_buffer[ts_str] for ts_str in json_buffer})
-
-TimeStampedBufferType = TypeVar('TimeStampedBuffer', bound="TimeStampedBuffer")
-# Type definition for type annotation convenience
-
-class PupilDiameterAnalyzer():
- """Abstract class to define what should provide a pupil diameter analyser."""
-
- def analyze(self, ts, pupil_diameter) -> float:
- """Analyze pupil diameter from successive timestamped pupil diameters."""
-
- raise NotImplementedError('analyze() method not implemented')
-
- def browse(self, ts_pupil_diameters: TimeStampedPupilDiameters) -> TimeStampedBufferType:
- """Analyze by browsing timestamped pupil diameters."""
-
- assert(type(ts_pupil_diameters) == TimeStampedPupilDiameters)
-
- ts_analyzis = DataStructures.TimeStampedBuffer()
-
- # Iterate on pupil diameters
- for ts, pupil_diameter in ts_pupil_diameters.items():
-
- analysis = self.analyze(ts, pupil_diameter)
-
- if analysis is not None:
-
- ts_analyzis[ts] = analysis
-
- return ts_analyzis
diff --git a/src/argaze/PupilAnalysis/WorkloadIndex.py b/src/argaze/PupillAnalysis/WorkloadIndex.py
index f86a9e8..4a20091 100644
--- a/src/argaze/PupilAnalysis/WorkloadIndex.py
+++ b/src/argaze/PupillAnalysis/WorkloadIndex.py
@@ -11,15 +11,15 @@ from typing import TypeVar
from dataclasses import dataclass, field
import math
-from argaze import PupilFeatures
+from argaze import PupillFeatures
import numpy
@dataclass
-class PupilDiameterAnalyzer(PupilFeatures.PupilDiameterAnalyzer):
- """Periodic average of pupil diameter variations to pupil diameter reference value."""
+class PupillDiameterAnalyzer(PupillFeatures.PupillDiameterAnalyzer):
+ """Periodic average of pupill diameter variations to pupill diameter reference value."""
- reference: PupilFeatures.PupilDiameter
+ reference: PupillFeatures.PupillDiameter
""" """
period: int | float = field(default=1)
@@ -33,11 +33,11 @@ class PupilDiameterAnalyzer(PupilFeatures.PupilDiameterAnalyzer):
self.__variations_number = 0
self.__last_ts = 0
- def analyze(self, ts, pupil_diameter) -> float:
- """Analyze workload index from successive timestamped pupil diameters."""
+ def analyze(self, ts, pupill_diameter) -> float:
+ """Analyze workload index from successive timestamped pupill diameters."""
- # Ignore non valid pupil diameter
- if not pupil_diameter.valid:
+ # Ignore non valid pupill diameter
+ if not pupill_diameter.valid:
return None
@@ -51,7 +51,7 @@ class PupilDiameterAnalyzer(PupilFeatures.PupilDiameterAnalyzer):
workload_index = 0.
- self.__variations_sum = pupil_diameter.value - self.reference.value
+ self.__variations_sum = pupill_diameter.value - self.reference.value
self.__variations_number = 1
self.__last_ts = ts
@@ -59,6 +59,6 @@ class PupilDiameterAnalyzer(PupilFeatures.PupilDiameterAnalyzer):
else:
- self.__variations_sum += pupil_diameter.value - self.reference.value
+ self.__variations_sum += pupill_diameter.value - self.reference.value
self.__variations_number += 1
\ No newline at end of file
diff --git a/src/argaze/PupillAnalysis/__init__.py b/src/argaze/PupillAnalysis/__init__.py
new file mode 100644
index 0000000..18f0f15
--- /dev/null
+++ b/src/argaze/PupillAnalysis/__init__.py
@@ -0,0 +1,4 @@
+"""
+Class interface to work with various pupill analysis algorithms.
+"""
+__all__ = ['WorkloadIndex'] \ No newline at end of file
diff --git a/src/argaze/PupillFeatures.py b/src/argaze/PupillFeatures.py
new file mode 100644
index 0000000..05408a3
--- /dev/null
+++ b/src/argaze/PupillFeatures.py
@@ -0,0 +1,106 @@
+#!/usr/bin/env python
+
+""" """
+
+__author__ = "Théo de la Hogue"
+__credits__ = []
+__copyright__ = "Copyright 2023, Ecole Nationale de l'Aviation Civile (ENAC)"
+__license__ = "BSD"
+
+from typing import TypeVar
+from dataclasses import dataclass, field
+import json
+
+from argaze import DataStructures
+
+@dataclass(frozen=True)
+class PupillDiameter():
+ """Define pupill diameter as ..."""
+
+ value: float = field(default=0.)
+ """Pupill diameter value."""
+
+ @property
+ def valid(self) -> bool:
+ """Is the value not 0"""
+
+ return self.value != 0.
+
+ def __repr__(self):
+ """String representation"""
+
+ return json.dumps(self, ensure_ascii = False, default=vars)
+
+class UnvalidPupillDiameter(PupillDiameter):
+ """Unvalid pupill diameter."""
+
+ def __init__(self, message=None):
+
+ self.message = message
+
+ super().__init__(0.)
+
+TimeStampedPupillDiametersType = TypeVar('TimeStampedPupillDiameters', bound="TimeStampedPupillDiameters")
+# Type definition for type annotation convenience
+
+class TimeStampedPupillDiameters(DataStructures.TimeStampedBuffer):
+ """Define timestamped buffer to store pupill diameters."""
+
+ def __setitem__(self, key, value: PupillDiameter|dict):
+ """Force PupillDiameter storage."""
+
+ # Convert dict into PupillDiameter
+ if type(value) == dict:
+
+ assert(set(['value']).issubset(value.keys()))
+
+ if 'message' in value.keys():
+
+ value = UnvalidPupillDiameter(value['message'])
+
+ else:
+
+ value = PupillDiameter(value['value'])
+
+ assert(type(value) == PupillDiameter or type(value) == UnvalidPupillDiameter)
+
+ super().__setitem__(key, value)
+
+ @classmethod
+ def from_json(self, json_filepath: str) -> TimeStampedPupillDiametersType:
+ """Create a TimeStampedPupillDiametersType from .json file."""
+
+ with open(json_filepath, encoding='utf-8') as ts_buffer_file:
+
+ json_buffer = json.load(ts_buffer_file)
+
+ return TimeStampedPupillDiameters({ast.literal_eval(ts_str): json_buffer[ts_str] for ts_str in json_buffer})
+
+TimeStampedBufferType = TypeVar('TimeStampedBuffer', bound="TimeStampedBuffer")
+# Type definition for type annotation convenience
+
+class PupillDiameterAnalyzer():
+ """Abstract class to define what should provide a pupill diameter analyser."""
+
+ def analyze(self, ts, pupill_diameter) -> float:
+ """Analyze pupill diameter from successive timestamped pupill diameters."""
+
+ raise NotImplementedError('analyze() method not implemented')
+
+ def browse(self, ts_pupill_diameters: TimeStampedPupillDiameters) -> TimeStampedBufferType:
+ """Analyze by browsing timestamped pupill diameters."""
+
+ assert(type(ts_pupill_diameters) == TimeStampedPupillDiameters)
+
+ ts_analyzis = DataStructures.TimeStampedBuffer()
+
+ # Iterate on pupill diameters
+ for ts, pupill_diameter in ts_pupill_diameters.items():
+
+ analysis = self.analyze(ts, pupill_diameter)
+
+ if analysis is not None:
+
+ ts_analyzis[ts] = analysis
+
+ return ts_analyzis
diff --git a/src/argaze/__init__.py b/src/argaze/__init__.py
index 5b2685b..9f79a17 100644
--- a/src/argaze/__init__.py
+++ b/src/argaze/__init__.py
@@ -1,4 +1,4 @@
"""
ArGaze is divided in submodules dedicated to various specifics features.
"""
-__all__ = ['ArUcoMarkers','AreaOfInterest','ArFeatures','GazeFeatures','GazeAnalysis','PupilFeatures','PupilAnalysis','DataStructures','utils'] \ No newline at end of file
+__all__ = ['ArUcoMarkers','AreaOfInterest','ArFeatures','GazeFeatures','GazeAnalysis','PupillFeatures','PupillAnalysis','DataStructures','utils'] \ No newline at end of file