geone 1.3.0__py313-none-manylinux_2_35_x86_64.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.
geone/markovChain.py ADDED
@@ -0,0 +1,666 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ # -------------------------------------------------------------------------
5
+ # Python module: 'markovChain.py'
6
+ # author: Julien Straubhaar
7
+ # date: sep-2024
8
+ # -------------------------------------------------------------------------
9
+
10
+ """
11
+ Module for simulation of Markov Chain on finite sets of states.
12
+ """
13
+
14
+ import numpy as np
15
+
16
+ # ============================================================================
17
+ class MarkovChainError(Exception):
18
+ """
19
+ Custom exception related to `markovChain` module.
20
+ """
21
+ pass
22
+ # ============================================================================
23
+
24
+ # ----------------------------------------------------------------------------
25
+ def mc_kernel1(n, p, return_pinv=False):
26
+ """
27
+ Sets the following symmetric transition kernel of order n for a Markov chain:
28
+
29
+ .. math::
30
+ P = \\left(\\begin{array}{cccc}
31
+ p & \\frac{1-p}{n-1} & \\ldots & \\frac{1-p}{n-1}\\\\
32
+ \\frac{1-p}{n-1} & \\ddots & \\ddots & \\vdots\\\\
33
+ \\vdots & \\ddots & \\ddots & \\frac{1-p}{n-1}\\\\
34
+ \\frac{1-p}{n-1} & \\ldots & \\frac{1-p}{n-1} & p
35
+ \\end{array}\\right)
36
+
37
+ where :math:`0\\leqslant p < 1` is a parameter.
38
+
39
+ Parameters
40
+ ----------
41
+ n : int
42
+ order of the kernel, number of states
43
+
44
+ p : float
45
+ number in the interval [0, 1[
46
+
47
+ return_pinv : bool
48
+ indicates if the invariant distribution is returned
49
+
50
+ Returns
51
+ -------
52
+ kernel : 2d-array of shape (n, n)
53
+ transition kernel (P) above
54
+
55
+ pinv : (1d-array of shape (n,)
56
+ invariant distibution of the kernel, that is
57
+
58
+ - [1/n, ... , 1/n]
59
+
60
+ returned if `return_pinv=True`
61
+ """
62
+ # fname = 'mc_kernel1'
63
+
64
+ if p < 0 or p >= 1:
65
+ return None
66
+ r = (1.0 - p)/(n-1)
67
+ x = [p] + (n-1)*[r]
68
+ kernel = np.array([x[-i:] + x[0:-i] for i in range(n)])
69
+ if return_pinv:
70
+ pinv = 1/n * np.ones(n)
71
+ if return_pinv:
72
+ out = kernel, pinv
73
+ else:
74
+ out = kernel
75
+ return out
76
+ # ----------------------------------------------------------------------------
77
+
78
+ # ----------------------------------------------------------------------------
79
+ def mc_kernel2(n, p, return_pinv=False):
80
+ """
81
+ Sets the following transition kernel of order n for a Markov chain:
82
+
83
+ .. math::
84
+ P = \\left(\\begin{array}{cccccc}
85
+ p & 1-p & 0 & \\ldots & 0 & 0\\\\
86
+ \\frac{1-p}{2} & p & \\frac{1-p}{2} & \\ddots & & 0\\\\
87
+ 0 & \\ddots & \\ddots & \\ddots & \\ddots & \\vdots\\\\
88
+ \\vdots & \\ddots & \\ddots & \\ddots & \\ddots & 0\\\\
89
+ 0 & & \\ddots & \\frac{1-p}{2} & p & \\frac{1-p}{2}\\\\
90
+ 0 & 0 & \\ldots & 0 & 1-p & p
91
+ \\end{array}\\right)
92
+
93
+ where :math:`0\\leqslant p < 1` is a parameter.
94
+
95
+ Parameters
96
+ ----------
97
+ n : int
98
+ order of the kernel, number of states
99
+
100
+ p : float
101
+ number in the interval [0, 1[
102
+
103
+ return_pinv : bool
104
+ indicates if the invariant distribution is returned
105
+
106
+ Returns
107
+ -------
108
+ kernel : 2d-array of shape (n, n)
109
+ transition kernel (P) above
110
+
111
+ pinv : (1d-array of shape (n,)
112
+ invariant distibution of the kernel, that is
113
+
114
+ - [1/(2(n-1)), 1/(n-1), ... , 1/(n-1), 1/(2(n-1))]
115
+
116
+ returned if `return_pinv=True`
117
+ """
118
+ # fname = 'mc_kernel2'
119
+
120
+ if p < 0 or p >= 1:
121
+ return None
122
+ r = (1.0 - p)/2.0
123
+ xa = [p, 2*r] + (n-2)*[0]
124
+ x = [r, p, r] + (n-3)*[0]
125
+ xb = (n-2)*[0] + [2*r, p]
126
+ kernel = np.array([xa] + [x[-i:] + x[0:-i] for i in range(n-2)] + [xb])
127
+ if return_pinv:
128
+ t = 1.0 / (2.0*(n-1))
129
+ pinv = np.hstack((np.array([t]), 1/(n-1) * np.ones(n-2), np.array([t])))
130
+ if return_pinv:
131
+ out = kernel, pinv
132
+ else:
133
+ out = kernel
134
+ return out
135
+ # ----------------------------------------------------------------------------
136
+
137
+ # ----------------------------------------------------------------------------
138
+ def mc_kernel3(n, p, q, return_pinv=False):
139
+ """
140
+ Sets the following transition kernel of order n for a Markov chain:
141
+
142
+ .. math::
143
+ P = \\left(\\begin{array}{ccccc}
144
+ p & (1-p)q & 0 & \\ldots & (1-p)(1-q)\\\\
145
+ (1-p)(1-q) & \\ddots & \\ddots & & 0\\\\
146
+ 0 & \\ddots & \\ddots & \\ddots & \\vdots\\\\
147
+ \\vdots & \\ddots & \\ddots & \\ddots & 0\\\\
148
+ 0 & & \\ddots & \\ddots & (1-p)q\\\\
149
+ (1-p)q & 0 & \\ldots & (1-p)(1-q) & p
150
+ \\end{array}\\right)
151
+
152
+ where :math:`0\\leqslant p < 1` and :math:`0\\leqslant q \\leqslant 1` are
153
+ two parameters.
154
+
155
+ Parameters
156
+ ----------
157
+ n : int
158
+ order of the kernel, number of states
159
+
160
+ p : float
161
+ number in the interval [0, 1[
162
+
163
+ q : float
164
+ number in the interval [0, 1]
165
+
166
+ return_pinv : bool
167
+ indicates if the invariant distribution is returned
168
+
169
+ Returns
170
+ -------
171
+ kernel : 2d-array of shape (n, n)
172
+ transition kernel (P) above
173
+
174
+ pinv : (1d-array of shape (n,)
175
+ invariant distibution of the kernel, that is
176
+
177
+ - [1/n, ... , 1/n]
178
+
179
+ returned if `return_pinv=True`
180
+ """
181
+ # fname = 'mc_kernel3'
182
+
183
+ if p < 0 or p >= 1 or q < 0 or q > 1:
184
+ return None
185
+ r = (1.0 - p)*q
186
+ s = (1.0 - p)*(1.0 - q)
187
+ x = [p, r] + (n-3)*[0] + [s]
188
+ kernel = np.array([x[-i:] + x[0:-i] for i in range(n)])
189
+ if return_pinv:
190
+ pinv = 1/n * np.ones(n)
191
+ if return_pinv:
192
+ out = kernel, pinv
193
+ else:
194
+ out = kernel
195
+ return out
196
+ # ----------------------------------------------------------------------------
197
+
198
+ # ----------------------------------------------------------------------------
199
+ def mc_kernel4(n, p, q, return_pinv=False):
200
+ """
201
+ Sets the following transition kernel of order n for a Markov chain:
202
+
203
+ .. math::
204
+ P = \\left(\\begin{array}{ccccc}
205
+ q & 0 & \\ldots & 0 & 1-q\\\\
206
+ 0 & \\ddots & \\ddots & \\vdots & \\vdots\\\\
207
+ \\vdots & \\ddots & \\ddots & 0 & \\vdots\\\\
208
+ 0 & \\ldots & 0 & q & 1-q\\\\
209
+ \\frac{1-p}{n-1} & \\ldots & \\ldots &\\frac{1-p}{n-1} & p
210
+ \\end{array}\\right)
211
+
212
+ where :math:`0\\leqslant p, q < 1` are two parameters.
213
+
214
+ Parameters
215
+ ----------
216
+ n : int
217
+ order of the kernel, number of states
218
+
219
+ p : float
220
+ number in the interval [0, 1[
221
+
222
+ q : float
223
+ number in the interval [0, 1[
224
+
225
+ return_pinv : bool
226
+ indicates if the invariant distribution is returned
227
+
228
+ Returns
229
+ -------
230
+ kernel : 2d-array of shape (n, n)
231
+ transition kernel (P) above
232
+
233
+ pinv : (1d-array of shape (n,)
234
+ invariant distibution of the kernel, that is
235
+
236
+ - 1/(2-p-q) * [(1-p)/(n-1), ... , (1-p)/(n-1), 1-q]
237
+
238
+ returned if `return_pinv=True`
239
+ """
240
+ # fname = 'mc_kernel4'
241
+
242
+ if p < 0 or p >= 1 or q < 0 or q >= 1:
243
+ return None
244
+ kernel = np.vstack((np.hstack((np.diag(q*np.ones(n-1)), (1-q)*np.ones((n-1, 1)))),[(n-1)*[(1-p)/(n-1)]+[p]]))
245
+ if return_pinv:
246
+ pinv = 1/(2-p-q)*np.array((n-1)*[(1-p)/(n-1)] + [1-q])
247
+ if return_pinv:
248
+ out = kernel, pinv
249
+ else:
250
+ out = kernel
251
+ return out
252
+ # ----------------------------------------------------------------------------
253
+
254
+ # ----------------------------------------------------------------------------
255
+ def compute_mc_pinv(kernel, logger=None):
256
+ """
257
+ Computes the invariant distribution of a Markov chain for a given kernel.
258
+
259
+ Parameters
260
+ ----------
261
+ kernel : 2d-array of shape (n, n)
262
+ transition kernel of a Markov chain on a set of states
263
+ :math:`S=\\{0, \\ldots, n-1\\}`; the element at row `i` and column `j` is
264
+ the probability to have the state of index `j` at the next step given
265
+ the state `i` at the current step, i.e.
266
+
267
+ - :math:`kernel[i][j] = P(X_{k+1}=j\\ \\vert\\ X_{k}=i)`
268
+
269
+ where the sequence of random variables :math:`(X_k)` is a Markov chain
270
+ on `S` defined by the kernel `kernel`.
271
+
272
+ In particular, every element of `kernel` is positive or zero, and its
273
+ rows sum to one.
274
+
275
+ logger : :class:`logging.Logger`, optional
276
+ logger (see package `logging`)
277
+ if specified, messages are written via `logger` (no print)
278
+
279
+ Returns
280
+ -------
281
+ pinv : 1d-array of shape (n,)
282
+ invariant distribution of the Markov chain, i.e. `pinv` is an eigen
283
+ vector of eigen value `1` of the transpose of `kernel`:
284
+ :math:`pinv \\cdot kernel= pinv`; note:
285
+
286
+ - `None` is returned if no eigen value is equal to 1
287
+ - if more than one eigen value is equal to 1, only the first corresponding\
288
+ eigen vector computed by `numpy.linalg.eig` is returned
289
+ """
290
+ fname = 'compute_mc_pinv'
291
+
292
+ valt, st = np.linalg.eig(kernel.T)
293
+ ind = np.where(np.isclose(valt, 1.0))[0]
294
+ if len(ind) == 0:
295
+ err_msg = f'{fname}: no invariant distribution found'
296
+ if logger: logger.error(err_msg)
297
+ raise MarkovChainError(err_msg)
298
+
299
+ pinv = np.real(st[:,ind[0]]/np.sum(st[:,ind[0]]))
300
+ return pinv
301
+ # ----------------------------------------------------------------------------
302
+
303
+ # ----------------------------------------------------------------------------
304
+ def compute_mc_kernel_rev(kernel, pinv=None, logger=None):
305
+ """
306
+ Computes the reverse transition kernel of a Markov chain for a given kernel.
307
+
308
+ Parameters
309
+ ----------
310
+ kernel : 2d-array of shape (n, n)
311
+ transition kernel of a Markov chain on a set of states
312
+ :math:`S=\\{0, \\ldots, n-1\\}`; the element at row `i` and column `j` is
313
+ the probability to have the state of index `j` at the next step given
314
+ the state `i` at the current step, i.e.
315
+
316
+ - :math:`kernel[i][j] = P(X_{k+1}=j\\ \\vert\\ X_{k}=i)`
317
+
318
+ where the sequence of random variables :math:`(X_k)` is a Markov chain
319
+ on `S` defined by the kernel `kernel`.
320
+
321
+ In particular, every element of `kernel` is positive or zero, and its
322
+ rows sum to one.
323
+
324
+ pinv : 1d-array of shape (n,), optional
325
+ invariant distribution of the Markov chain;
326
+ by default (`None`): `pinv` is automatically computed
327
+
328
+ logger : :class:`logging.Logger`, optional
329
+ logger (see package `logging`)
330
+ if specified, messages are written via `logger` (no print)
331
+
332
+ Returns
333
+ -------
334
+ kernel_rev : 2d-array of shape (n, n)
335
+ reverse transition kernel of the Markov chain, i.e.
336
+
337
+ - :math:`kernel\\_rev[i, j] = pinv[i]^{-1} \\cdot kernel[j, i] \\cdot pinv[j]`
338
+ """
339
+ fname = 'compute_mc_kernel_rev'
340
+
341
+ if pinv is None:
342
+ try:
343
+ pinv = compute_mc_pinv(kernel, logger=logger)
344
+ except Exception as exc:
345
+ err_msg = f'{fname}: computing invariant distribution failed'
346
+ if logger: logger.error(err_msg)
347
+ raise MarkovChainError(err_msg) from exc
348
+
349
+ if pinv is None or np.any(np.isclose(pinv, 0)):
350
+ err_msg = f'{fname}: kernel not reversible'
351
+ if logger: logger.error(err_msg)
352
+ raise MarkovChainError(err_msg)
353
+
354
+ kernel_rev = (kernel/pinv).T*pinv
355
+
356
+ return kernel_rev
357
+ # ----------------------------------------------------------------------------
358
+
359
+ # ----------------------------------------------------------------------------
360
+ def compute_mc_cov(kernel, pinv=None, nsteps=1, logger=None):
361
+ """
362
+ Computes covariances of indicators for a Markov chain, accross time steps.
363
+
364
+ Parameters
365
+ ----------
366
+ kernel : 2d-array of shape (n, n)
367
+ transition kernel of a Markov chain on a set of states
368
+ :math:`S=\\{0, \\ldots, n-1\\}`; the element at row `i` and column `j` is
369
+ the probability to have the state of index `j` at the next step given
370
+ the state `i` at the current step, i.e.
371
+
372
+ - :math:`kernel[i][j] = P(X_{k+1}=j\\ \\vert\\ X_{k}=i)`
373
+
374
+ where the sequence of random variables :math:`(X_k)` is a Markov chain
375
+ on `S` defined by the kernel `kernel`.
376
+
377
+ In particular, every element of `kernel` is positive or zero, and its
378
+ rows sum to one.
379
+
380
+ pinv : 1d-array of shape (n,), optional
381
+ invariant distribution of the Markov chain;
382
+ by default (`None`): `pinv` is automatically computed
383
+
384
+ nsteps : int, default: 1
385
+ number of (time) steps for which the covariance is computed
386
+
387
+ logger : :class:`logging.Logger`, optional
388
+ logger (see package `logging`)
389
+ if specified, messages are written via `logger` (no print)
390
+
391
+ Returns
392
+ -------
393
+ mc_cov : 3d-array of shape (nstep, n, n)
394
+ covariances of indicators for a Markov chain X built from the kernel
395
+ and its invariant distribution:
396
+
397
+ - :math:`mc\\_cov[l, i, j] = \\operatorname{Cov}(X_k=i, X_{k+l}=j) = pinv[i] \\cdot ((kernel^l)[i,j]- pinv[j])`
398
+
399
+ for `l=0, \\ldots, nsteps-1`, and :math:`0\\leqslant i, j \\leqslant n-1`
400
+ """
401
+ fname = 'compute_mc_cov'
402
+
403
+ # Diagonalization of kernel: kernel = s.dot(diag(val)).dot(sinv)
404
+ val, s = np.linalg.eig(kernel)
405
+ val = np.real(val) # take the real part (if needed)
406
+ s = np.real(s) # take the real part (if needed)
407
+ sinv = np.linalg.inv(s)
408
+
409
+ if pinv is None:
410
+ try:
411
+ pinv = compute_mc_pinv(kernel, logger=logger)
412
+ except Exception as exc:
413
+ err_msg = f'{fname}: computing invariant distribution failed'
414
+ if logger: logger.error(err_msg)
415
+ raise MarkovChainError(err_msg) from exc
416
+
417
+ n = kernel.shape[0]
418
+ mc_cov = np.zeros((nsteps, n, n))
419
+ for m in range(nsteps):
420
+ km = s.dot(np.diag(val**m)).dot(sinv)
421
+ mc_cov[m, :, :] = np.repeat(pinv, n).reshape(n, n)*km - np.outer(pinv, pinv)
422
+
423
+ return mc_cov
424
+ # ----------------------------------------------------------------------------
425
+
426
+ # ----------------------------------------------------------------------------
427
+ def simulate_mc(
428
+ kernel,
429
+ nsteps,
430
+ categVal=None,
431
+ data_ind=None, data_val=None,
432
+ pstep0=None,
433
+ pinv=None,
434
+ kernel_rev=None, kernel_pow=None,
435
+ nreal=1,
436
+ logger=None):
437
+ """
438
+ Generates (conditional) Markov chains for a given kernel.
439
+
440
+ Parameters
441
+ ----------
442
+ kernel : 2d-array of shape (n, n)
443
+ transition kernel of a Markov chain on a set of states
444
+ :math:`S=\\{0, \\ldots, n-1\\}`; the element at row `i` and column `j` is
445
+ the probability to have the state of index `j` at the next step given
446
+ the state `i` at the current step, i.e.
447
+
448
+ - :math:`kernel[i][j] = P(X_{k+1}=j\\ \\vert\\ X_{k}=i)`
449
+
450
+ where the sequence of random variables :math:`(X_k)` is a Markov chain
451
+ on `S` defined by the kernel `kernel`.
452
+
453
+ In particular, every element of `kernel` is positive or zero, and its
454
+ rows sum to one.
455
+
456
+ nsteps : int
457
+ length of each generated chain, number of time steps
458
+
459
+ categVal :1d-array of shape (n,), optional
460
+ values of categories (one value for each state `0, ..., n-1`);
461
+ by default (`None`) : `categVal` is set to `[0, ..., n-1]`
462
+
463
+ data_ind : sequence, optional
464
+ index (time step) of conditioning locations; each index should be in
465
+ `{0, 1, ..., nsteps-1}`
466
+
467
+ data_val : sequence, optional
468
+ values at conditioning locations (same length as `data_ind`); each value
469
+ should be in `categVal`
470
+
471
+ pstep0 : 1d-array of shape (n,), optional
472
+ distribution for step 0 of the chain, for unconditional simulation
473
+ (used only if `data_ind=None` and `data_val=None`)
474
+ by default (`None`): `pinv` is used (see below)
475
+
476
+ pinv : 1d-array of shape (n,), optional
477
+ invariant distribution of the Markov chain;
478
+ by default (`None`): `pinv` is automatically computed
479
+
480
+ kernel_rev : 2d-array of shape (n, n), optional
481
+ reverse transition kernel of the Markov chain;
482
+ by default (`None`): `kernel_rev` is automatically computed;
483
+ note: only used for conditional simulation
484
+
485
+ kernel_pow : 3d-array of shape (m, n, n), optional
486
+ pre-computed kernel raised to power 0, 1, ..., m-1:
487
+
488
+ - :math:`kernel\\_pow[k] = kernel^k`
489
+
490
+ note: only used for conditional simulation
491
+
492
+ nreal : int, default: 1
493
+ number of realization(s), number of generated chain(s)
494
+
495
+ logger : :class:`logging.Logger`, optional
496
+ logger (see package `logging`)
497
+ if specified, messages are written via `logger` (no print)
498
+
499
+ Returns
500
+ -------
501
+ x : 3d-array of shape (nreal, nsteps)
502
+ generated Markov chain (conditional to `data_ind, data_val` if present), `x[i]` is
503
+ the i-th realization
504
+ """
505
+ fname = 'simulate_mc'
506
+
507
+ # Number of categories (order of the kernel)
508
+ n = kernel.shape[0]
509
+
510
+ # Check category values
511
+ if categVal is None:
512
+ categVal = np.arange(n)
513
+ else:
514
+ categVal = np.asarray(categVal)
515
+ if categVal.ndim != 1 or categVal.shape[0] != n:
516
+ err_msg = f'{fname}: `categVal` invalid'
517
+ if logger: logger.error(err_msg)
518
+ raise MarkovChainError(err_msg)
519
+
520
+ if len(np.unique(categVal)) != len(categVal):
521
+ err_msg = f'{fname}: `categVal` contains duplicated values'
522
+ if logger: logger.error(err_msg)
523
+ raise MarkovChainError(err_msg)
524
+
525
+ # Conditioning
526
+ if (data_ind is None and data_val is not None) or (data_ind is not None and data_val is None):
527
+ err_msg = f'{fname}: `data_ind` and `data_val` must both be specified'
528
+ if logger: logger.error(err_msg)
529
+ raise MarkovChainError(err_msg)
530
+
531
+ if data_ind is None:
532
+ nhd = 0
533
+ else:
534
+ data_ind = np.asarray(data_ind, dtype='int').reshape(-1) # cast in 1-dimensional array if needed
535
+ data_val = np.asarray(data_val, dtype='float').reshape(-1) # cast in 1-dimensional array if needed
536
+ nhd = len(data_ind)
537
+ if len(data_ind) != len(data_val):
538
+ err_msg = f'{fname}: length of `data_ind` and length of `data_val` differ'
539
+ if logger: logger.error(err_msg)
540
+ raise MarkovChainError(err_msg)
541
+
542
+ # Check values
543
+ if not np.all([xv in categVal for xv in data_val]):
544
+ err_msg = f'{fname}: `data_val` contains an invalid value'
545
+ if logger: logger.error(err_msg)
546
+ raise MarkovChainError(err_msg)
547
+
548
+ # Replace values by their index in categVal
549
+ data_val = np.array([np.where(categVal==xv)[0][0] for xv in data_val], dtype='int')
550
+
551
+ if nhd == 0:
552
+ # Compute invariant distribution for kernel
553
+ if pinv is None:
554
+ try:
555
+ pinv = compute_mc_pinv(kernel, logger=logger)
556
+ except Exception as exc:
557
+ err_msg = f'{fname}: computing invariant distribution failed'
558
+ if logger: logger.error(err_msg)
559
+ raise MarkovChainError(err_msg) from exc
560
+
561
+ # Compute cdf for x0
562
+ if pstep0 is not None:
563
+ x0_cdf = np.cumsum(pstep0)
564
+ else:
565
+ x0_cdf = np.cumsum(pinv)
566
+
567
+ # Compute conditional cdf from the transition kernel
568
+ kernel_cdf = np.cumsum(kernel, axis=1)
569
+
570
+ # Generate X
571
+ x = []
572
+ for _ in range(nreal):
573
+ # Generate one chain (realization)
574
+ xk = - np.ones(nsteps, dtype='int')
575
+ u = np.random.random(size=nsteps)
576
+ xk[0] = np.where(u[0] < x0_cdf)[0][0]
577
+ for i in range(1, nsteps):
578
+ xk[i] = np.where(u[i] < kernel_cdf[int(xk[i-1])])[0][0]
579
+ # Append realization to x
580
+ x.append(xk)
581
+
582
+ else:
583
+ inds = np.argsort(data_ind)
584
+ if data_ind[inds[0]] > 0:
585
+ # Compute reverse transition kernel
586
+ if kernel_rev is None:
587
+ try:
588
+ kernel_rev = compute_mc_kernel_rev(kernel, pinv=pinv, logger=logger)
589
+ except Exception as exc:
590
+ err_msg = f'{fname}: computing reverse transition kernel failed'
591
+ if logger: logger.error(err_msg)
592
+ raise MarkovChainError(err_msg) from exc
593
+
594
+ # Compute conditional cdf from the reverse transition kernel
595
+ kernel_rev_cdf = np.cumsum(kernel_rev, axis=1)
596
+
597
+ if data_ind[inds[-1]] < nsteps-1:
598
+ # compute conditional cdf from the transition kernel
599
+ kernel_cdf = np.cumsum(kernel, axis=1)
600
+
601
+ if nhd > 1:
602
+ # Compute list of kernel raise to power 0, 1, 2, ..., m-1 for further computation
603
+ m = max(np.diff([data_ind[j] for j in inds])) + 1
604
+ if kernel_pow is None:
605
+ kernel_pow = np.zeros((m, n, n))
606
+ kernel_pow[0] = np.eye(n)
607
+ m0 = 1
608
+ else:
609
+ m0 = kernel_pow.shape[0]
610
+ if m0 < m:
611
+ kernel_pow = np.concatenate((kernel_pow, np.zeros((m-m0, n, n))), axis=0)
612
+ for i in range(m0, m):
613
+ kernel_pow[i] = kernel_pow[i-1].dot(kernel)
614
+ #
615
+ # With k1 < k2 < k3, we have:
616
+ # Prob(x[k2]=i2 | x[k1]=i1, x[k3]=i3) = kernel^(k2-k1)[i1, i2] * kernel^(k3-k2)[i2, i3] / kernel^(k3-k1)[i1, i3]
617
+ # Check validity of conditioning points:
618
+ # check that the denominator above is positive for each pair (k1, k3) of consecutive conditioning points, i.e.
619
+ # Prob(x[k1]=i1 | x[k3]=i3) = kernel^(k3-k1)[i1, i3] > 0
620
+ if np.any(np.isclose([kernel_pow[data_ind[inds[i+1]]-data_ind[inds[i]], int(data_val[inds[i]]), int(data_val[inds[i+1]])] for i in range(nhd-1)], 0)):
621
+ # if np.any([kernel_pow[data_ind[inds[i+1]]-data_ind[inds[i]], int(data_val[inds[i]]), int(data_val[inds[i+1]])] < 1.e-20 for i in range(nhd-1)]):
622
+ err_msg = f'{fname}: invalid conditioning points wrt. kernel'
623
+ if logger: logger.error(err_msg)
624
+ raise MarkovChainError(err_msg)
625
+
626
+ # Generate X
627
+ x = []
628
+ for _ in range(nreal):
629
+ # Generate one chain (realization)
630
+ # Initialization
631
+ xk = - np.ones(nsteps, dtype='int')
632
+ xk[data_ind] = data_val
633
+ #
634
+ # Random numbers in [0,1[
635
+ u = np.random.random(size=nsteps)
636
+ #
637
+ # Simulate in reverse order the values before the first conditioning point (sorted)
638
+ for i in range(data_ind[inds[0]]-1, -1, -1):
639
+ xk[i] = np.where(u[i] < kernel_rev_cdf[int(xk[i+1])])[0][0]
640
+ # Simulate the values between the pairs of consecutive conditioning points (sorted)
641
+ # With k1 < k2 < k3, we have:
642
+ # Prob(x[k2]=i2 | x[k1]=i1, x[k3]=i3) = kernel^(k2-k1)[i1, i2] * kernel^(k3-k2)[i2, i3] / kernel^(k3-k1)[i1, i3]
643
+ for ii in range(nhd-1):
644
+ # Simulate the values between the ii-th and (ii+1)-th conditioning points (sorted)
645
+ xend_ind = data_ind[inds[ii+1]]
646
+ xend_val = int(data_val[inds[ii+1]])
647
+ for i in range(data_ind[inds[ii]]+1, data_ind[inds[ii+1]]):
648
+ prob = np.array([kernel_pow[1, int(xk[i-1]), j]*kernel_pow[xend_ind-i, j, xend_val]/kernel_pow[xend_ind-i+1, int(xk[i-1]), xend_val] for j in range(n)])
649
+ cdf = np.cumsum(prob)
650
+ # if i==11:
651
+ # print(k, i, kernel_pow[xend_ind-i+1, int(xk[i-1]), xend_val], prob, cdf)
652
+ xk[i] = np.where(u[i] < cdf)[0][0]
653
+ # Simulate the values after the last conditioning point (sorted)
654
+ for i in range(data_ind[inds[-1]]+1, nsteps):
655
+ xk[i] = np.where(u[i] < kernel_cdf[int(xk[i-1])])[0][0]
656
+
657
+ # Append realization to x
658
+ x.append(xk)
659
+
660
+ x = np.asarray(x)
661
+
662
+ # Set original values
663
+ x = categVal[x]
664
+
665
+ return x
666
+ # ----------------------------------------------------------------------------