InfluenceDiffusion 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,436 @@
1
+ from typing import List, Union, Dict
2
+ import os
3
+ import pickle
4
+ import numpy as np
5
+ from tqdm import tqdm
6
+ from joblib import Parallel, delayed
7
+ from scipy.stats import uniform
8
+ from scipy.stats._distn_infrastructure import rv_frozen
9
+
10
+ from ..Trace import Traces, PseudoTraces
11
+ from ..Graph import Graph
12
+ from ..utils import multiple_union
13
+
14
+
15
+ class BaseWeightEstimator:
16
+ """Base class for weight estimation in influence models.
17
+
18
+ Attributes
19
+ ----------
20
+ graph : Graph
21
+ The graph representing the network.
22
+ n_jobs : Optional[int]
23
+ Number of jobs for parallel processing.
24
+ informative_vertices : set
25
+ Set of informative vertices in the graph.
26
+ weights_ : np.ndarray
27
+ Estimated weights for edges.
28
+ _vertex_2_num_traces_was_informative : Dict[int, int]
29
+ Mapping of vertices to the number of informative traces they participated in.
30
+ _failed_vertices_masks : Dict[int, np.ndarray]
31
+ Masks for failed vertices.
32
+ _vertex_2_active_parent_mask_tm1 : Dict[int, np.ndarray]
33
+ Masks for active parents at time t-1.
34
+ _vertex_2_active_parent_mask_t : Dict[int, np.ndarray]
35
+ Masks for active parents at time t.
36
+ """
37
+
38
+ def __init__(self, graph: Graph, n_jobs: int = None):
39
+ self.graph = graph
40
+ self.n_jobs = n_jobs
41
+ self.informative_vertices = set()
42
+ self.weights_ = np.array([])
43
+
44
+ def _precompute_num_traces_vertices_participated(self) -> None:
45
+ """Precompute the number of traces each vertex participated in."""
46
+ self._vertex_2_num_traces_was_informative = {}
47
+ for vertex in self.informative_vertices:
48
+ num_failed_traces = 0 if vertex not in self._failed_vertices_masks \
49
+ else self._failed_vertices_masks[vertex].shape[0]
50
+ num_activated_traces = 0 if vertex not in self._vertex_2_active_parent_mask_tm1 \
51
+ else self._vertex_2_active_parent_mask_tm1[vertex].shape[0]
52
+ self._vertex_2_num_traces_was_informative[vertex] = num_activated_traces + num_failed_traces
53
+
54
+ def _precompute_failed_vertices_parents_masks_from_traces(self, traces: Traces) -> None:
55
+ """Compute masks for failed vertices based on traces.
56
+
57
+ Parameters
58
+ ----------
59
+ traces : Traces
60
+ The traces to analyze.
61
+ """
62
+ failed_vertices_masks = {vertex: [] for vertex in self.informative_vertices}
63
+ for trace in traces:
64
+ active_vertices = np.array(list(trace.get_all_activated_vertices()))
65
+ failed_vertices = np.array(list(trace.get_all_failed_vertices()))
66
+ for vertex in failed_vertices:
67
+ parents = self.graph.get_parents(vertex, out_type=np.array)
68
+ active_parents_mask = np.in1d(parents, active_vertices)
69
+ failed_vertices_masks[vertex].append(active_parents_mask)
70
+
71
+ self._failed_vertices_masks = {vertex: np.vstack(masks) for vertex, masks in failed_vertices_masks.items()
72
+ if len(masks) > 0}
73
+
74
+ def _precompute_active_vertices_parents_masks_from_traces(self, traces: Traces) -> None:
75
+ """Compute active vertices' parents masks from traces.
76
+
77
+ Parameters
78
+ ----------
79
+ traces : Traces
80
+ The traces to analyze.
81
+ """
82
+ vertex_2_active_parent_mask_tm1 = {vertex: [] for vertex in self.informative_vertices}
83
+ vertex_2_active_parent_mask_t = {vertex: [] for vertex in self.informative_vertices}
84
+
85
+ for trace in traces:
86
+ cum_trace_list = [set()] + list(trace.cum_union())
87
+ for new_vertices_tp1, vertices_t, vertices_tm1 in zip(trace[1:], cum_trace_list[1:-1], cum_trace_list[:-2]):
88
+ for vertex in new_vertices_tp1:
89
+ parents = self.graph.get_parents(vertex, out_type=np.array)
90
+ active_parents_mask_t = np.in1d(parents, np.array(list(vertices_t)))
91
+ active_parents_mask_tm1 = np.in1d(parents, np.array(list(vertices_tm1)))
92
+ vertex_2_active_parent_mask_tm1[vertex].append(active_parents_mask_tm1)
93
+ vertex_2_active_parent_mask_t[vertex].append(active_parents_mask_t)
94
+
95
+ self._vertex_2_active_parent_mask_tm1 = {vertex: np.vstack(masks)
96
+ for vertex, masks in vertex_2_active_parent_mask_tm1.items()
97
+ if len(masks) > 0}
98
+ self._vertex_2_active_parent_mask_t = {vertex: np.vstack(masks)
99
+ for vertex, masks in vertex_2_active_parent_mask_t.items()
100
+ if len(masks) > 0}
101
+
102
+ def _load_masks(self, masks_path: str) -> None:
103
+ """Load masks from specified file paths.
104
+
105
+ Parameters
106
+ ----------
107
+ masks_path : str
108
+ Path to the directory containing mask files.
109
+ """
110
+ with open(os.path.join(masks_path, "activated_masks_tm1.pkl"), "rb") as f:
111
+ self._vertex_2_active_parent_mask_tm1 = pickle.load(f)
112
+ with open(os.path.join(masks_path, "activated_masks_t.pkl"), "rb") as f:
113
+ self._vertex_2_active_parent_mask_t = pickle.load(f)
114
+ with open(os.path.join(masks_path, "failed_masks.pkl"), "rb") as f:
115
+ self._failed_vertices_masks = pickle.load(f)
116
+
117
+ def _precompute_all_informative_vertices(self, traces: Union[Traces, PseudoTraces]) -> None:
118
+ """Precompute all informative vertices from traces.
119
+
120
+ Parameters
121
+ ----------
122
+ traces : Union[Traces, PseudoTraces]
123
+ The traces to analyze.
124
+ """
125
+ if isinstance(traces, Traces):
126
+ self.informative_vertices = multiple_union(
127
+ [trace.get_all_failed_and_activated_vertices_no_seed() for trace in traces])
128
+ elif isinstance(traces, PseudoTraces):
129
+ self.informative_vertices = set(traces.keys())
130
+ else:
131
+ raise NotImplementedError("traces should be of either Traces or PseudoTraces types")
132
+
133
+ def _precompute_vertices_parents_masks_from_pseudo_traces(self, traces: PseudoTraces) -> None:
134
+ """Compute parents masks from pseudo traces.
135
+
136
+ Parameters
137
+ ----------
138
+ traces : PseudoTraces
139
+ The pseudo traces to analyze.
140
+ """
141
+ vertex_2_active_parent_mask_tm1 = {vertex: [] for vertex in self.informative_vertices}
142
+ vertex_2_active_parent_mask_t = {vertex: [] for vertex in self.informative_vertices}
143
+ failed_vertices_masks = {vertex: [] for vertex in self.informative_vertices}
144
+
145
+ for vertex, vertex_pseudo_traces in tqdm(traces.items()):
146
+ for vertices_tm1, new_vertices_t in vertex_pseudo_traces:
147
+ parents = self.graph.get_parents(vertex, out_type=np.array)
148
+ active_parents_mask_tm1 = np.in1d(parents, np.array(list(vertices_tm1)))
149
+ if new_vertices_t:
150
+ vertices_t = vertices_tm1 | new_vertices_t
151
+ active_parents_mask_t = np.in1d(parents, np.array(list(vertices_t)))
152
+ vertex_2_active_parent_mask_tm1[vertex].append(active_parents_mask_tm1)
153
+ vertex_2_active_parent_mask_t[vertex].append(active_parents_mask_t)
154
+ else:
155
+ failed_vertices_masks[vertex].append(active_parents_mask_tm1)
156
+
157
+ self._failed_vertices_masks = {vertex: np.vstack(masks)
158
+ for vertex, masks in failed_vertices_masks.items() if len(masks) > 0}
159
+ self._vertex_2_active_parent_mask_tm1 = {vertex: np.vstack(masks)
160
+ for vertex, masks in vertex_2_active_parent_mask_tm1.items()
161
+ if len(masks) > 0}
162
+ self._vertex_2_active_parent_mask_t = {vertex: np.vstack(masks)
163
+ for vertex, masks in vertex_2_active_parent_mask_t.items()
164
+ if len(masks) > 0}
165
+
166
+ def _precompute_vertices_parents_masks(self, traces: Union[Traces, PseudoTraces]) -> None:
167
+ """Precompute vertices' parents masks based on traces.
168
+
169
+ Parameters
170
+ ----------
171
+ traces : Union[Traces, PseudoTraces]
172
+ The traces to analyze.
173
+ """
174
+ if isinstance(traces, Traces):
175
+ self._precompute_failed_vertices_parents_masks_from_traces(traces)
176
+ self._precompute_active_vertices_parents_masks_from_traces(traces)
177
+ elif isinstance(traces, PseudoTraces):
178
+ self._precompute_vertices_parents_masks_from_pseudo_traces(traces)
179
+ else:
180
+ raise NotImplementedError("traces can only be of type Traces or PseudoTraces")
181
+
182
+ def _preprocess_traces(self, traces: Union[Traces, PseudoTraces], masks_path: str = None) -> None:
183
+ """Preprocess traces by validating and computing masks.
184
+
185
+ Parameters
186
+ ----------
187
+ traces : Union[Traces, PseudoTraces]
188
+ The traces to preprocess.
189
+ masks_path : Optional[str]
190
+ Path to load masks from, if applicable.
191
+ """
192
+ traces = self._validate_traces(traces)
193
+ self._precompute_all_informative_vertices(traces)
194
+ if masks_path is None:
195
+ self._precompute_vertices_parents_masks(traces)
196
+ else:
197
+ self._load_masks(masks_path)
198
+ self._precompute_num_traces_vertices_participated()
199
+
200
+ def _validate_traces(self, traces) -> Union[Traces, PseudoTraces]:
201
+ """Validate and convert traces to Traces or PseudoTraces.
202
+
203
+ Parameters
204
+ ----------
205
+ traces : any
206
+ The traces to validate.
207
+
208
+ Returns
209
+ -------
210
+ Union[Traces, PseudoTraces]
211
+ The validated traces.
212
+ """
213
+ try:
214
+ return Traces(self.graph, traces)
215
+ except:
216
+ return PseudoTraces(traces)
217
+
218
+ def _check_init_weight_correctness(self, init_weights: Union[List, np.ndarray]) -> None:
219
+ """Check the correctness of initial weights.
220
+
221
+ Parameters
222
+ ----------
223
+ init_weights : Union[List, np.ndarray]
224
+ Initial weights to validate.
225
+
226
+ Raises
227
+ ------
228
+ AssertionError
229
+ If the length of the weights does not match the number of edges.
230
+ """
231
+ assert len(init_weights) == self.graph.count_edges()
232
+
233
+ def _init_weights(self, init_weights: Union[List, np.ndarray]) -> None:
234
+ """Initialize weights based on provided initial weights or generate random weights.
235
+
236
+ Parameters
237
+ ----------
238
+ init_weights : Optional[Union[List, np.ndarray]]
239
+ Initial weights to set. If None, random weights are generated.
240
+ """
241
+ if init_weights is not None:
242
+ self._check_init_weight_correctness(init_weights)
243
+ self.weights_ = np.array(init_weights)
244
+ else:
245
+ self.weights_ = self._generate_random_weights()
246
+
247
+ def _generate_random_weights(self) -> np.ndarray:
248
+ """Generate random weights for edges.
249
+
250
+ Returns
251
+ -------
252
+ np.ndarray
253
+ Randomly generated weights.
254
+ """
255
+ raise NotImplementedError("Subclasses should implement this method.")
256
+
257
+ def _optimize_vertex_parent_params(self, vertex: int, **optim_kwargs) -> np.ndarray:
258
+ """Optimize parameters for a specific vertex.
259
+
260
+ Parameters
261
+ ----------
262
+ vertex : int
263
+ The vertex to optimize parameters for.
264
+ optim_kwargs : dict
265
+ Additional optimization parameters.
266
+
267
+ Returns
268
+ -------
269
+ np.ndarray
270
+ Optimized parameters for the vertex.
271
+ """
272
+ raise NotImplementedError("Subclasses should implement this method.")
273
+
274
+ def _optimize_informative_vertices_parent_params(self, verbose: bool = False, **optim_kwargs) -> List[np.ndarray]:
275
+ """Optimize parameters for all informative vertices in parallel.
276
+
277
+ Parameters
278
+ ----------
279
+ verbose : bool, optional
280
+ If True, progress messages are printed.
281
+ optim_kwargs : dict
282
+ Additional optimization parameters.
283
+
284
+ Returns
285
+ -------
286
+ List[np.ndarray]
287
+ List of optimized parameters for each informative vertex.
288
+ """
289
+ informative_vertices_params = Parallel(n_jobs=self.n_jobs, verbose=verbose)(
290
+ delayed(self._optimize_vertex_parent_params)(vertex, **optim_kwargs)
291
+ for vertex in self.informative_vertices)
292
+ return informative_vertices_params
293
+
294
+ def _set_informative_vertices_parent_params(self, informative_vertices_params: List[np.ndarray]) -> None:
295
+ """Set the optimized parameters for informative vertices.
296
+
297
+ Parameters
298
+ ----------
299
+ informative_vertices_params : List[np.ndarray]
300
+ List of optimized parameters for each informative vertex.
301
+ """
302
+ for vertex, parent_weights in zip(self.informative_vertices, informative_vertices_params):
303
+ self.weights_[self.graph.get_parents_mask(vertex)] = parent_weights
304
+
305
+ def _pre_fit(self, traces: Union[Traces, PseudoTraces],
306
+ init_weights: Union[List, np.ndarray] = None,
307
+ masks_path: str = None) -> None:
308
+ """Prepare for fitting by preprocessing traces and initializing weights.
309
+
310
+ Parameters
311
+ ----------
312
+ traces : Union[Traces, PseudoTraces]
313
+ The traces to analyze.
314
+ init_weights : Optional[Union[List, np.ndarray]]
315
+ Initial weights to set.
316
+ masks_path : Optional[str]
317
+ Path to load masks from.
318
+ """
319
+ self._preprocess_traces(traces, masks_path=masks_path)
320
+ self._init_weights(init_weights)
321
+
322
+ def fit(self, traces: Union[Traces, PseudoTraces],
323
+ init_weights: Union[List, np.ndarray] = None,
324
+ verbose: bool = False,
325
+ masks_path: str = None,
326
+ **optim_kwargs) -> np.ndarray:
327
+ """Fit the model to the given traces.
328
+
329
+ Parameters
330
+ ----------
331
+ traces : Union[Traces, PseudoTraces]
332
+ The traces to analyze.
333
+ init_weights : Optional[Union[List, np.ndarray]]
334
+ Initial weights to set.
335
+ verbose : bool, optional
336
+ If True, progress messages are printed.
337
+ masks_path : Optional[str]
338
+ Path to load masks from.
339
+ optim_kwargs : dict
340
+ Additional optimization parameters.
341
+
342
+ Returns
343
+ -------
344
+ np.ndarray
345
+ The final estimated weights.
346
+ """
347
+ self._pre_fit(traces, init_weights=init_weights, masks_path=masks_path)
348
+ informative_vertices_params = self._optimize_informative_vertices_parent_params(verbose=verbose, **optim_kwargs)
349
+ self._set_informative_vertices_parent_params(informative_vertices_params)
350
+ return self.weights_
351
+
352
+
353
+ class BaseGLTWEightEstimator(BaseWeightEstimator):
354
+ """Base class for GLTW weight estimation.
355
+
356
+ Attributes
357
+ ----------
358
+ vertex_2_distrib : Dict[int, scipy.stats.rv_frozen]
359
+ Distribution mapping for vertices.
360
+ """
361
+
362
+ def __init__(self, graph: Graph, n_jobs: int = None,
363
+ vertex_2_distrib: Dict[int, rv_frozen] = None):
364
+ super().__init__(graph, n_jobs)
365
+ if vertex_2_distrib is None:
366
+ vertex_2_distrib = {v: uniform(0, 1) for v in graph.get_vertices()}
367
+ self.vertex_2_distrib = vertex_2_distrib
368
+
369
+ def _generate_random_weights(self) -> np.ndarray:
370
+ """Generate random weights based on vertex distributions.
371
+
372
+ Returns
373
+ -------
374
+ np.ndarray
375
+ Randomly generated weights for edges.
376
+ """
377
+ weights = np.zeros(self.graph.count_edges())
378
+ for vertex in self.informative_vertices:
379
+ parent_mask = self.graph.get_parents_mask(vertex)
380
+ num_parents = self.graph.get_indegree(vertex, weighted=False)
381
+ support_ub = self.vertex_2_distrib[vertex].support()[1]
382
+ if support_ub == np.inf:
383
+ weights[parent_mask] = np.random.exponential(num_parents)
384
+ else:
385
+ weights[parent_mask] = np.random.rand(num_parents) * support_ub / num_parents
386
+ return weights
387
+
388
+ def _check_init_weight_correctness(self, init_weights: Union[List, np.ndarray], eps: float = 1e-6) -> None:
389
+ """Check the correctness of initial weights for GLTM.
390
+
391
+ Parameters
392
+ ----------
393
+ init_weights : Union[List, np.ndarray]
394
+ Initial weights to validate.
395
+ eps : float, optional
396
+ Tolerance for weight validation.
397
+
398
+ Raises
399
+ ------
400
+ AssertionError
401
+ If initial weights do not meet the criteria.
402
+ """
403
+ super()._check_init_weight_correctness(init_weights)
404
+ indegrees = np.array([init_weights[self.graph.get_parents_mask(vertex)].sum()
405
+ for vertex in self.vertex_2_distrib])
406
+ vertex_supports = np.vstack([distrib.support() for vertex, distrib in self.vertex_2_distrib.items()])
407
+ assert np.all((indegrees >= vertex_supports[:, 0] - eps) & (indegrees <= vertex_supports[:, 1] + eps))
408
+
409
+
410
+ class BaseICWEightEstimator(BaseWeightEstimator):
411
+ """Base class for Independent Cascade Model weight estimation."""
412
+
413
+ def _generate_random_weights(self) -> np.ndarray:
414
+ """Generate random weights for edges uniformly distributed between 0 and 1.
415
+
416
+ Returns
417
+ -------
418
+ np.ndarray
419
+ Randomly generated weights for edges.
420
+ """
421
+ return np.random.rand(self.graph.count_edges())
422
+
423
+ def _check_init_weight_correctness(self, init_weights: Union[List, np.ndarray]) -> None:
424
+ """Check the correctness of initial weights for the Independent Cascade Model.
425
+
426
+ Parameters
427
+ ----------
428
+ init_weights : Union[List, np.ndarray]
429
+ Initial weights to validate.
430
+
431
+ Raises
432
+ ------
433
+ AssertionError
434
+ If initial weights are not within the range [0, 1].
435
+ """
436
+ assert np.all((init_weights >= 0) & (init_weights <= 1))
@@ -0,0 +1,236 @@
1
+ import numpy as np
2
+ from typing import Union
3
+
4
+ from BaseWeightEstimator import BaseWeightEstimator, BaseGLTWEightEstimator, BaseICWEightEstimator
5
+ from ..Trace import Traces, PseudoTraces
6
+ from ..utils import invert_non_zeros
7
+ from ..Graph import Graph
8
+
9
+
10
+ class BaseWeightEstimatorEM(BaseWeightEstimator):
11
+ """Base class for EM weight estimators.
12
+
13
+ This class implements the EM algorithm for optimizing parent weights of vertices in a graph.
14
+
15
+ Methods
16
+ -------
17
+ _em_step(vertex, vertex_parent_weights)
18
+ Performs a single EM step for the specified vertex.
19
+ _optimize_vertex_parent_params(vertex, max_iter=50, tol=1e-3)
20
+ Optimizes the parent weights for the specified vertex using the EM algorithm.
21
+ """
22
+
23
+ def _em_step(self, vertex: int, vertex_parent_weights: np.array) -> np.array:
24
+ """Perform a single EM step for the specified vertex.
25
+
26
+ Parameters
27
+ ----------
28
+ vertex : int
29
+ The vertex for which to perform the EM step.
30
+ vertex_parent_weights : np.array
31
+ The current weights of the parent vertices.
32
+
33
+ Returns
34
+ -------
35
+ np.array
36
+ The updated weights for the parent vertices.
37
+ """
38
+ raise NotImplementedError()
39
+
40
+ def _optimize_vertex_parent_params(self, vertex: int, max_iter: int = 50, tol: float = 1e-3) -> np.array:
41
+ """Optimize the parent weights for the specified vertex using the EM algorithm.
42
+
43
+ Parameters
44
+ ----------
45
+ vertex : int
46
+ The vertex for which to optimize parent weights.
47
+ max_iter : int, optional
48
+ The maximum number of iterations (default is 50).
49
+ tol : float, optional
50
+ The tolerance for convergence (default is 1e-3).
51
+
52
+ Returns
53
+ -------
54
+ np.array
55
+ The optimized weights for the parent vertices.
56
+ """
57
+ parent_weights = self.weights_[self.graph.get_parents_mask(vertex)]
58
+
59
+ for _ in range(max_iter):
60
+ new_parent_weights = self._em_step(vertex=vertex, vertex_parent_weights=parent_weights)
61
+ parent_weight_change = np.linalg.norm(new_parent_weights - parent_weights)
62
+ parent_weights = new_parent_weights
63
+ if parent_weight_change < tol:
64
+ break
65
+ return parent_weights
66
+
67
+
68
+ class ICWeightEstimatorEM(BaseICWEightEstimator, BaseWeightEstimatorEM):
69
+ """Independent Cascade Weight Estimator using EM algorithm.
70
+
71
+ This class extends the base EM weight estimator for the Independent Cascade model.
72
+
73
+ Methods
74
+ -------
75
+ _precompute_num_traces_parents_activated()
76
+ Precomputes the number of traces where parents were activated for each vertex.
77
+ _em_step(vertex, vertex_parent_weights)
78
+ Performs a single EM step for the specified vertex.
79
+ _preprocess_traces(traces: Union[Traces, PseudoTraces], masks_path: str = None)
80
+ Preprocesses the input traces and prepares necessary data for EM steps.
81
+ _generate_random_weights()
82
+ Generates random initial weights for the parent vertices.
83
+ """
84
+
85
+ def _precompute_num_traces_parents_activated(self) -> None:
86
+ """Precompute the number of traces where parents were activated for each vertex.
87
+
88
+ This method updates the internal state to keep track of how many traces
89
+ activated the parent vertices of the informative vertices.
90
+ """
91
+ self._vertex_2_num_traces_parents_activated = {}
92
+ for vertex in self.informative_vertices:
93
+ n_traces_parents_activated = np.zeros(self.graph.get_indegree(vertex))
94
+ if vertex in self._vertex_2_active_parent_mask_t:
95
+ n_traces_parents_activated += self._vertex_2_active_parent_mask_t[vertex].sum(0)
96
+ if vertex in self._failed_vertices_masks:
97
+ n_traces_parents_activated += self._failed_vertices_masks[vertex].sum(0)
98
+ self._vertex_2_num_traces_parents_activated[vertex] = n_traces_parents_activated
99
+
100
+ def _em_step(self, vertex: int, vertex_parent_weights: np.array) -> np.array:
101
+ """Perform a single EM step for the specified vertex.
102
+
103
+ This method computes the updated weights based on the current parent weights.
104
+
105
+ Parameters
106
+ ----------
107
+ vertex : int
108
+ The vertex for which to perform the EM step.
109
+ vertex_parent_weights : np.array
110
+ The current weights of the parent vertices.
111
+
112
+ Returns
113
+ -------
114
+ np.array
115
+ The updated weights for the parent vertices.
116
+ """
117
+ if vertex not in self._vertex_2_active_parent_mask_t:
118
+ return np.zeros_like(vertex_parent_weights)
119
+
120
+ active_parents_mask_t = self._vertex_2_active_parent_mask_t[vertex]
121
+ active_parents_mask_tm1 = self._vertex_2_active_parent_mask_tm1[vertex]
122
+ new_active_parents_mask = active_parents_mask_t & (~active_parents_mask_tm1)
123
+
124
+ no_activation_probs = new_active_parents_mask * (1. - vertex_parent_weights)
125
+ probs_vertex_activated = 1. - np.prod(no_activation_probs, axis=1, where=no_activation_probs > 0, keepdims=True)
126
+ inv_probs_vertex_activated = invert_non_zeros(probs_vertex_activated)
127
+ upd_weights = vertex_parent_weights * (new_active_parents_mask * inv_probs_vertex_activated).sum(0)
128
+
129
+ n_traces_parents_activated = self._vertex_2_num_traces_parents_activated[vertex]
130
+ seen_parents_mask = n_traces_parents_activated != 0
131
+ upd_weights[seen_parents_mask] /= n_traces_parents_activated[seen_parents_mask]
132
+ return np.clip(upd_weights, 0, 1)
133
+
134
+ def _preprocess_traces(self, traces: Union[Traces, PseudoTraces], masks_path: str = None) -> None:
135
+ """Preprocesses the input traces and prepares necessary data for EM steps.
136
+
137
+ Parameters
138
+ ----------
139
+ traces : Union[Traces, PseudoTraces]
140
+ The input traces or pseudotraces to analyze.
141
+ masks_path : str, optional
142
+ The path to load masks from (default is None).
143
+ """
144
+ super()._preprocess_traces(traces=traces, masks_path=masks_path)
145
+ self._precompute_num_traces_parents_activated()
146
+
147
+ def _generate_random_weights(self) -> np.array:
148
+ """Generates random initial weights for the parent vertices.
149
+
150
+ Returns
151
+ -------
152
+ np.array
153
+ An array of random weights for the parent vertices.
154
+ """
155
+ weights = np.zeros(self.graph.count_edges())
156
+ informative_edge_mask = np.isin(self.graph.edge_array[:, 1], list(self.informative_vertices))
157
+ weights[informative_edge_mask] = np.random.rand(informative_edge_mask.sum())
158
+ return weights
159
+
160
+
161
+ class LTWeightEstimatorEM(BaseGLTWEightEstimator, BaseWeightEstimatorEM):
162
+ """Linear Threshold Weight Estimator using EM algorithm.
163
+
164
+ This class extends the base EM weight estimator for the Linear Threshold model.
165
+
166
+ Methods
167
+ -------
168
+ _em_step(vertex, vertex_parent_weights)
169
+ Performs a single EM step for the specified vertex.
170
+ _generate_random_weights()
171
+ Generates random initial weights for the parent vertices.
172
+ """
173
+
174
+ def __init__(self, graph: Graph, n_jobs: int = None):
175
+ """Initialize the Linear Threshold Weight Estimator.
176
+
177
+ Parameters
178
+ ----------
179
+ graph : Graph
180
+ The graph representing the relationships between vertices.
181
+ n_jobs : int, optional
182
+ The number of jobs for parallel processing (default is None).
183
+ """
184
+ super().__init__(graph, n_jobs=n_jobs)
185
+
186
+ def _em_step(self, vertex: int, vertex_parent_weights: np.array) -> np.array:
187
+ """Perform a single EM step for the specified vertex.
188
+
189
+ Parameters
190
+ ----------
191
+ vertex : int
192
+ The vertex for which to perform the EM step.
193
+ vertex_parent_weights : np.array
194
+ The current weights of the parent vertices.
195
+
196
+ Returns
197
+ -------
198
+ np.array
199
+ The updated weights for the parent vertices.
200
+ """
201
+ if vertex in self._vertex_2_active_parent_mask_t:
202
+ active_parents_mask_t = self._vertex_2_active_parent_mask_t[vertex]
203
+ active_parents_mask_tm1 = self._vertex_2_active_parent_mask_tm1[vertex]
204
+ new_active_parents_mask = active_parents_mask_t & (~active_parents_mask_tm1)
205
+ probs_vertex_activated = (new_active_parents_mask * vertex_parent_weights).sum(1).reshape(-1, 1)
206
+ inv_probs_vertex_activated = invert_non_zeros(probs_vertex_activated)
207
+ H_uvs = vertex_parent_weights * (new_active_parents_mask * inv_probs_vertex_activated).sum(0)
208
+ else:
209
+ H_uvs = np.zeros_like(vertex_parent_weights)
210
+
211
+ if vertex in self._failed_vertices_masks:
212
+ active_parents_T = self._failed_vertices_masks[vertex]
213
+ probs_vertex_failed = 1. - (vertex_parent_weights * active_parents_T).sum(1).reshape(-1, 1)
214
+ inv_probs_vertex_failed = invert_non_zeros(probs_vertex_failed)
215
+ H_empty_v = (1. - vertex_parent_weights.sum()) * inv_probs_vertex_failed.sum()
216
+ H_uvs += vertex_parent_weights * ((~active_parents_T) * inv_probs_vertex_failed).sum(0)
217
+ else:
218
+ H_empty_v = 0.
219
+
220
+ upd_weights = H_uvs / (H_empty_v + H_uvs.sum())
221
+ return upd_weights
222
+
223
+ def _generate_random_weights(self) -> np.array:
224
+ """Generates random initial weights for the parent vertices.
225
+
226
+ Returns
227
+ -------
228
+ np.array
229
+ An array of random weights for the parent vertices.
230
+ """
231
+ weights = np.zeros(self.graph.count_edges())
232
+ for vertex in self.informative_vertices:
233
+ parent_mask = self.graph.get_parents_mask(vertex)
234
+ num_parents = self.graph.get_indegree(vertex, weighted=False)
235
+ weights[parent_mask] = np.random.rand(num_parents) / num_parents
236
+ return weights