1
1
from __future__ import annotations
2
- from dataclasses import dataclass , field
3
2
from typing import List , TYPE_CHECKING
4
3
5
- from dataclasses_json . cfg import config
6
- import numpy
4
+ from attr import define , field
5
+ from numpy import array , linalg
7
6
from pandas import DataFrame
8
7
9
8
import cellengine as ce
10
- from cellengine .utils .dataclass_mixin import DataClassMixin , ReadOnly
9
+ from cellengine .utils import readonly
10
+ from cellengine .utils .converter import converter
11
11
12
12
13
13
if TYPE_CHECKING :
14
14
from cellengine .resources .fcs_file import FcsFile
15
15
16
16
17
- @dataclass
18
- class Compensation ( DataClassMixin ) :
17
+ @define
18
+ class Compensation :
19
19
"""A class representing a CellEngine compensation matrix.
20
20
21
21
Can be applied to FCS files to compensate them.
22
22
"""
23
23
24
+ _id : str = field (on_setattr = readonly )
25
+ experiment_id : str = field (on_setattr = readonly )
24
26
name : str
25
27
channels : List [str ]
26
- dataframe : DataFrame = field (
27
- metadata = config (
28
- field_name = "spillMatrix" ,
29
- encoder = lambda x : x .to_numpy ().flatten ().tolist (),
30
- decoder = lambda x : numpy .array (x ),
31
- )
32
- )
33
- _id : str = field (
34
- metadata = config (field_name = "_id" ), default = ReadOnly ()
35
- ) # type: ignore
36
- experiment_id : str = field (default = ReadOnly ()) # type: ignore
37
-
38
- def __post_init__ (self ):
39
- self .dataframe = DataFrame (
40
- self .dataframe .reshape (self .N , self .N ),
28
+ spill_matrix : List [float ]
29
+
30
+ @property
31
+ def dataframe (self ):
32
+ return DataFrame (
33
+ array (self .spill_matrix ).reshape (self .N , self .N ), # type: ignore
41
34
columns = self .channels ,
42
35
index = self .channels ,
43
36
)
44
37
38
+ @dataframe .setter
39
+ def dataframe (self , val : DataFrame ):
40
+ try :
41
+ assert len (val .columns ) == len (val .index )
42
+ assert all (val .columns == val .index )
43
+ self .channels = val .columns .to_list ()
44
+ self .spill_matrix = val .to_numpy ().flatten ().tolist ()
45
+ except Exception :
46
+ raise ValueError (
47
+ "Dataframe must be a square matrix with equivalent index and columns."
48
+ )
49
+
45
50
def __repr__ (self ):
46
51
return f"Compensation(_id='{ self ._id } ', name='{ self .name } ')"
47
52
48
53
@property
49
- def N (self ):
50
- return len (self .channels )
54
+ def path (self ):
55
+ return f"experiments/{ self .experiment_id } /compensations/{ self ._id } " .rstrip (
56
+ "/None"
57
+ )
58
+
59
+ @classmethod
60
+ def from_dict (cls , data : dict ):
61
+ return converter .structure (data , cls )
62
+
63
+ def to_dict (self ):
64
+ return converter .unstructure (self )
51
65
52
66
@classmethod
53
67
def get (cls , experiment_id : str , _id : str = None , name : str = None ) -> Compensation :
54
68
kwargs = {"name" : name } if name else {"_id" : _id }
55
69
return ce .APIClient ().get_compensation (experiment_id , ** kwargs )
56
70
57
71
@classmethod
58
- def create (cls , experiment_id : str , compensation : dict ) -> Compensation :
59
- """Creates a compensation
72
+ def create (
73
+ cls ,
74
+ experiment_id : str ,
75
+ name : str ,
76
+ channels : List [str ],
77
+ spill_matrix : List [float ],
78
+ ) -> Compensation :
79
+ """Create a new compensation for this experiment
60
80
61
81
Args:
62
- experiment_id: ID of experiment that this compensation belongs to.
63
- compensation: Dict containing `channels` and `spillMatrix` properties.
82
+ experiment_id (str): the ID of the experiment.
83
+ name (str): The name of the compensation.
84
+ channels (List[str]): The names of the channels to which this
85
+ compensation matrix applies.
86
+ spill_matrix (List[float]): The row-wise, square spillover matrix. The
87
+ length of the array must be the number of channels squared.
64
88
"""
65
- return ce .APIClient ().post_compensation (experiment_id , compensation )
89
+ body = {"name" : name , "channels" : channels , "spillMatrix" : spill_matrix }
90
+ return ce .APIClient ().post_compensation (experiment_id , body )
91
+
92
+ @property
93
+ def N (self ):
94
+ return len (self .channels )
66
95
67
96
@staticmethod
68
97
def from_spill_string (spill_string : str ) -> Compensation :
@@ -81,14 +110,12 @@ def from_spill_string(spill_string: str) -> Compensation:
81
110
"experimentId" : "" ,
82
111
"name" : "" ,
83
112
}
84
- return Compensation . from_dict (properties )
113
+ return converter . structure (properties , Compensation )
85
114
86
115
def update (self ):
87
116
"""Save changes to this Compensation to CellEngine."""
88
- res = ce .APIClient ().update_entity (
89
- self .experiment_id , self ._id , "compensations" , body = self .to_dict ()
90
- )
91
- self .__dict__ .update (Compensation .from_dict (res ).__dict__ )
117
+ res = ce .APIClient ().update (self )
118
+ self .__setstate__ (res .__getstate__ ()) # type: ignore
92
119
93
120
def delete (self ):
94
121
return ce .APIClient ().delete_entity (
@@ -132,7 +159,7 @@ def apply(self, file: FcsFile, inplace: bool = True, **kwargs):
132
159
if any (ix ):
133
160
copy = data .copy ()
134
161
comped = copy [ix ]
135
- comped = comped .dot (numpy . linalg .inv (self .dataframe )) # type: ignore
162
+ comped = comped .dot (linalg .inv (self .dataframe )) # type: ignore
136
163
comped .columns = ix
137
164
copy .update (comped .astype (comped .dtypes [0 ]))
138
165
else :
0 commit comments