File size: 34,355 Bytes
ce3dfc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7d98790
 
 
 
 
ce3dfc6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
import numpy as np
import pandas as pd
from numba import jit
import math
import json
import os
import sys
from specklepy.api.client import SpeckleClient
from specklepy.api.credentials import get_default_account, get_local_accounts
from specklepy.transports.server import ServerTransport
from specklepy.api import operations
from specklepy.objects.geometry import Polyline, Point
from specklepy.objects import Base
from specklepy.api import operations, models
from specklepy.transports.server import ServerTransport
import time
from functools import wraps



sys.path.append("RECODE_speckle_utils")


from .RECODE_speckle_utils import speckle_utils




# !!! lots of hard coded values in computeTrips !!!

# UTILS
def reconstruct_dataframe(alpha_low, alpha_med, alpha_high, original_df):
    # Define the mapping from original values to new alpha parameters
    value_to_alpha = {
        0.00191: alpha_low,
        0.00767: alpha_high,
        0.0038: alpha_med
    }

    # Check if each value is present at least once in the DataFrame
    for original_value in value_to_alpha.keys():
        if not (original_df == original_value).any().any():
            raise ValueError(f"Value {original_value} not found in the input DataFrame.")

    # Create a new DataFrame based on the original one
    new_df = original_df.copy()

    # Apply the mapping to each element in the DataFrame
    for original_value, new_value in value_to_alpha.items():
        new_df = new_df.replace(original_value, new_value)

    return new_df

def preprocess_dataFrame(df, headerRow_idx=0, numRowsStart_idx = None, numRowsEnd_idx=None, numColsStart_idx=None, numColsEnd_idx=None, rowNames_idx=None):
  df.columns = df.iloc[headerRow_idx] #Set the header
  if rowNames_idx is not None:
    df.index = df.iloc[:, rowNames_idx] #Set the row names
  df = df.iloc[numRowsStart_idx : numRowsEnd_idx, numColsStart_idx:numColsEnd_idx] #Slice the dataset to numerical data
  return df




def timeit(f):
    def timed(*args, **kw):
        ts = time.time()
        result = f(*args, **kw)
        te = time.time()
        print ('func:%r args:[%r, %r] took: %2.4f sec' % \
          (f.__name__, te-ts))
          #(f.__name__, args, kw, te-ts))
        return result
    return timed





def timing_decorator(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start_time = time.time()
        result = func(*args, **kwargs)
        end_time = time.time()

        duration = end_time - start_time
        timestamp = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(end_time))

        print(f"{func.__name__} took {duration:.4f} seconds. Finished at {timestamp}")
        return result

    return wrapper


# Function to compare two dataframes after converting and rounding
def compare_dataframes(df1, df2, decimals=8):
    # Function to convert DataFrame columns to float and then round
    def convert_and_round_dataframe(df, decimals):
        # Convert all columns to float
        df_float = df.astype(float)
        # Round to the specified number of decimals
        return df_float.round(decimals)

    rounded_df1 = convert_and_round_dataframe(df1, decimals)
    rounded_df2 = convert_and_round_dataframe(df2, decimals)

    are_equal = rounded_df1.equals(rounded_df2)

    print("Both methods are equal:", are_equal)

    print("Numba shape:", df2.shape)
    print("Original shape:", df1.shape)

    print("======== ORIGINAL OUTPUT (first item in output list, head() for the first 5 columns)")
    print(df1.iloc[0:5].head(2))

    print("======== New method OUTPUT (first item in output list, head() for the first 5 columns)")
    print(df2.iloc[0:5].head(2))


def align_dataframes(df1, df2, key):
    """
    Align two dataframes based on a common key, ensuring that both dataframes
    have only the rows with matching keys.

    Parameters:
    - df1: First dataframe.
    - df2: Second dataframe.
    - key: Column name to align dataframes on.

    Returns:
    - df1_aligned, df2_aligned: Tuple of aligned dataframes.
    """
    common_ids = df1.index.intersection(df2[key])
    df1_aligned = df1.loc[common_ids]
    df2_aligned = df2[df2[key].isin(common_ids)].set_index(key, drop=False)
    return df1_aligned, df2_aligned



#==================================================================================================

def attraction_proNode(df_attraction_num, df_lu, df_lu_anName=None, printSteps=False):
  #lu_proNode
  df_lu_proNode = df_lu.reset_index()[df_lu_anName]
  if printSteps:
    print(df_lu_proNode.shape)
    df_lu_proNode.head(50)

  #attraction_proNode
  if printSteps:
    print("df_attraction_num:", df_attraction_num.shape)
    print("df_lu_proNode:", df_lu_proNode.shape)
  df_attraction_proNode = df_attraction_num.mul(df_lu_proNode, axis=0)
  if printSteps:
    print("df_attraction_proNode:", df_attraction_proNode.shape)
    df_attraction_proNode.head(100)

  # Sum the values of each column
  df_attraction_proNode_sum = pd.DataFrame(df_attraction_proNode.sum(), columns=['Sum'])
  if printSteps:
    print("df_attraction_proNode_sum:", df_attraction_proNode_sum.shape)
    df_attraction_proNode_sum.head(100)

  return df_attraction_proNode_sum

#Non vectorized iterative function
def attraction_proNode_full_iter(df_attraction_num, df_lu_num, printSteps=False):

  # Initialize an empty DataFrame
  df_attraction_proNode_sum_total = pd.DataFrame()

  for column_name, column_data in df_lu_num.items():
    df_attraction_proNode_sum = attraction_proNode(df_attraction_num, df_lu_num, df_lu_anName=column_name)

    # Concatenate DataFrames along columns
    df_attraction_proNode_sum_total = pd.concat([df_attraction_proNode_sum_total, df_attraction_proNode_sum], axis=1)

  # Rename columns in df_distBasedAttr_step2 with the same column names as in df_distributionMatrix_step1
  df_attraction_proNode_sum_total.columns = df_lu_num.columns

  return df_attraction_proNode_sum_total



# PRODUCTION ================================================ 

def production_proNode(df_lu, df_sqmProPerson, df_tripRate, df_production_num, df_production_transposed, printSteps=False, df_lu_anName=None):

  #lu_proNode - reset index
  df_lu_proNode = df_lu.reset_index()[df_lu_anName]
  if printSteps:
    print(df_lu_proNode.shape)
    df_lu_proNode.head(50)

  #Get the person count - Divide corresponding values of one DataFrame by another
  df_personCount = df_lu_proNode.div(df_sqmProPerson)
  if printSteps:
    print(df_personCount.shape)
    print(df_personCount)

  # Ensure the index is unique in df_personCount
  df_personCount = df_personCount.reset_index(drop=True)
  df_production_transposed = df_production_transposed.reset_index(drop=True)
  if printSteps:
    df_production_transposed.head()

  if printSteps:
    df_personCount.head()
    df_tripRate.head()


  #Calculate trip production pro node

  df_production_proNode = df_production_transposed
  df_production_proNode = df_production_proNode.mul(df_personCount, axis=0)


  df_production_proNode = df_production_proNode.T
  df_production_proNode = df_production_proNode.mul(df_tripRate, axis=0)


  #Total trips
  df_production_proNode_rowSum = df_production_proNode.sum(axis=1)
  df_total_trips = df_production_proNode_rowSum
  #if printSteps:
    #df_total_trips.head(50)

  return df_total_trips

#Non vectorized iterative function
def production_proNode_total(df_lu, df_sqmProPerson, df_tripRate, df_production_num, df_production_transposed, df_lu_num, printSteps=False):

  # Initialize an empty DataFrame
  df_total_trips_allNodes = pd.DataFrame()

  for column_name, column_data in df_lu_num.items():
    df_total_trips_proNode = production_proNode(df_lu_num, df_sqmProPerson, df_tripRate, df_production_num, df_production_transposed, printSteps=False, df_lu_anName=column_name)

    # Concatenate DataFrames along columns
    df_total_trips_allNodes = pd.concat([df_total_trips_allNodes, df_total_trips_proNode], axis=1)

  # Rename columns in df_distBasedAttr_step2 with the same column names as in df_distributionMatrix_step1
  df_total_trips_allNodes.columns = df_lu_num.columns

  return df_total_trips_allNodes


#df_total_trips_allNodes = production_proNode_total(df_lu, df_sqmProPerson, df_tripRate, df_production_num, df_production_transposed, df_lu_num, printSteps=False)
#df_total_trips_allNodes.head(50)

#==================================================================================================

#STEP 1
def step_1(df_distributionMatrix, df_total_trips_allNodes):
  l = []
  #counter=0
  for column_name, column_data in df_total_trips_allNodes.items():
    df_distributionMatrix_step1_proNode = df_distributionMatrix.mul(column_data, axis = 0)
    l.append(df_distributionMatrix_step1_proNode)

  return l

#STEP 2
def step_2_vectorized(df_distMatrix, df_alphas):
    # Convert df_distMatrix to a 2D array: Shape (1464, 1464)
    distMatrix_array = df_distMatrix.values

    # Convert df_alphas to a 1D array: Shape (26,)
    alphas_array = df_alphas.values

    # Initialize an empty array to store results: Shape (1464, 1464, 26)
    result_3d = np.zeros((distMatrix_array.shape[0], distMatrix_array.shape[1], len(alphas_array)))

    # Loop over alphas and perform element-wise multiplication followed by exponential function
    for i in range(len(alphas_array)):
        result_3d[:, :, i] = np.exp(-distMatrix_array * alphas_array[i])

    # Construct the final list of DataFrames
    final_list = [pd.DataFrame(result_3d[i, :, :], columns=df_alphas.index, index=df_distMatrix.index) for i in range(result_3d.shape[0])]

    return final_list

# Step 3
@jit(nopython=True)
def multiply_and_sum(arr, attraction_arr):
    # Element-wise multiplication
    multiplied_arr = arr * attraction_arr
    # Sum the values of each column
    summed_arr = multiplied_arr.sum(axis=0)
    return multiplied_arr, summed_arr

def step_3_numba(df_attraction_proNode_sum_total, df_step_2):
    # Convert df_attraction_proNode_sum_total to a NumPy array and transpose it
    attraction_array = df_attraction_proNode_sum_total.values.T.astype(np.float64)  # Ensure float64 dtype

    multiplied_results = []
    summed_results = []

    for df in df_step_2:
        # Convert DataFrame to NumPy array with float64 dtype
        df_array = df.values.astype(np.float64)

        # Perform element-wise multiplication and summing
        multiplied_arr, summed_arr = multiply_and_sum(df_array, attraction_array)

        # Convert results back to DataFrames
        df_multiplied = pd.DataFrame(multiplied_arr, columns=df.columns, index=df.index)

        # Reshape summed_arr to have shape (26,1) and then convert to DataFrame
        df_summed = pd.DataFrame(summed_arr.reshape(-1, 1), index=df.columns, columns=['Sum'])

        multiplied_results.append(df_multiplied)
        summed_results.append(df_summed)

    return multiplied_results, summed_results


# step 4
@jit(nopython=True)
def divide_and_sum(arr, divisor_arr):
    # Ensure divisor_arr is broadcastable to arr's shape
    divisor_arr_expanded = divisor_arr.reshape((divisor_arr.shape[0], 1, divisor_arr.shape[1]))

    # Initialize arrays to store results
    divided_result = np.zeros_like(arr)
    summed_result = np.zeros((arr.shape[0], arr.shape[2]))

    for i in range(arr.shape[0]):
        for j in range(arr.shape[1]):
            for k in range(arr.shape[2]):
                if divisor_arr_expanded[i, 0, k] != 0:
                    divided_result[i, j, k] = arr[i, j, k] / divisor_arr_expanded[i, 0, k]
                summed_result[i, k] += divided_result[i, j, k]

    return divided_result, summed_result

def step_4_numba(distAndAreaBasedAttr_step3, distAndAreaBasedAttr_step3_sum):
    # Convert lists of DataFrames to 3D arrays with dtype float64
    array_step3 = np.array([df.values for df in distAndAreaBasedAttr_step3]).astype(np.float64)
    array_step3_sum = np.array([df.values for df in distAndAreaBasedAttr_step3_sum]).astype(np.float64)

    # Perform division and summation using Numba
    divided_result, summed_result = divide_and_sum(array_step3, array_step3_sum)

    # Convert results back to lists of DataFrames
    df_distAndAreaBasedAttr_step4 = [pd.DataFrame(divided_result[i], columns=distAndAreaBasedAttr_step3[0].columns, index=distAndAreaBasedAttr_step3[0].index) for i in range(divided_result.shape[0])]

    # Correct the creation of the summed DataFrame to avoid setting the 'Sum' index
    df_distAndAreaBasedAttr_step4_sum = [pd.DataFrame(summed_result[i]).T.set_axis(['Sum'], axis='index').set_axis(distAndAreaBasedAttr_step3[0].columns, axis='columns') for i in range(summed_result.shape[0])]

    return df_distAndAreaBasedAttr_step4, df_distAndAreaBasedAttr_step4_sum

# step 5
@jit(nopython=True)
def tripsPerArctivity_numba(matrix, attrs):
    rows, cols = attrs.shape[0], matrix.shape[0]  # 1464, 26
    result = np.zeros((cols, rows), dtype=np.float64)  # Prepare result matrix (26, 1464)

    for i in range(rows):  # Iterate over each area
        for j in range(cols):  # Iterate over each land use category
            sum_val = 0.0
            for k in range(cols):  # Iterate over each element in the distribution matrix row
                sum_val += matrix[j, k] * attrs[i, k]
            result[j, i] = sum_val

    return result

def step_5_numba(distributionMatrix_step1, distAndAreaBasedAttr_step4):
    sums = []
    count = 0
    total_count = len(distributionMatrix_step1)

    for df_distributionMatrix_step1, df_distAndAreaBasedAttr_step4 in zip(distributionMatrix_step1, distAndAreaBasedAttr_step4):
        # Convert DataFrames to NumPy arrays with dtype float64
        matrix = df_distributionMatrix_step1.values.astype(np.float64)
        attrs = df_distAndAreaBasedAttr_step4.values.astype(np.float64)

        result = tripsPerArctivity_numba(matrix, attrs)
        df_result = pd.DataFrame(result, index=df_distributionMatrix_step1.columns, columns=df_distAndAreaBasedAttr_step4.index)

        sums.append(df_result)

        count += 1
        #print(f"Iteration {count} out of {total_count} is finished.")
        #print("---------")

    return sums


# step 6&7 
def step_6_7_vectorized(df_trips_proNode_proActivity_total):
    # Convert each DataFrame to a NumPy array and stack them to form a 3D array
    array_3d = np.array([df.values for df in df_trips_proNode_proActivity_total])

    # Sum across the middle axis (columns of each DataFrame)
    summed_array = array_3d.sum(axis=1)

    # Convert the summed array back to a DataFrame
    final_matrix = pd.DataFrame(summed_array, index=df_trips_proNode_proActivity_total[0].columns, columns=df_trips_proNode_proActivity_total[0].columns)

    return final_matrix


# step 8

def adjTripRate_adjFactor(tripMatrix,df_total_trips_allNodes_sumPerson, targetRate=1, factor=1 ):
  df_tripMatrix_total_sum = tripMatrix.sum().sum()
  df_total_trips_allNodes_sumPerson_total = df_total_trips_allNodes_sumPerson.sum()

  # scale to target trip rate
  tripRateBeforeAdjustment = df_tripMatrix_total_sum/df_total_trips_allNodes_sumPerson_total
  print("tripRateBeforeAdjustment",tripRateBeforeAdjustment)
  adjustmentRate = targetRate/tripRateBeforeAdjustment
  print("adjustmentRate",adjustmentRate)

  # scale by ... scale factor (outdated, was hardcoded )
  df_tripMatrix_adjusted = tripMatrix * adjustmentRate
  #df_tripMatrix_adjusted_scaled = df_tripMatrix_adjusted.div(factor)
  return df_tripMatrix_adjusted, df_tripMatrix_adjusted # df_tripMatrix_adjusted_scaled

# Uniform Matrix
def decay(d, alpha):
    return math.exp(d * alpha * -1)

def distanceDecay(df, alpha):
    return df.applymap(lambda x: decay(x, alpha))

def matrix_reduce_add(df):
    return df[df != sys.float_info.max].sum().sum()

def replace_maxValue(df):
    return df.replace(sys.float_info.max, 0)


#Trip gen matrix is used to scale the distance matrix
def getUniformMatrix(distanceMatrix, tripGenMatrix, alpha):

    distanceMatrix_withDecay = distanceDecay(distanceMatrix, alpha)
    distanceMatrix_sum = matrix_reduce_add(distanceMatrix_withDecay)
    tripGenMatrix_sum = matrix_reduce_add(tripGenMatrix)
    ratio = distanceMatrix_sum / tripGenMatrix_sum

    uniformMatrix = distanceMatrix_withDecay.div(ratio)

    return replace_maxValue(uniformMatrix)


#==================================================================================================
#Modal Split functions

def computeModalShare(trip_matrix, dist_matrices, alpha, f_values=None):
    """
    Process matrices or DataFrames with exponentiation and normalization.

    Args:
        trip_matrix (np.ndarray or pd.DataFrame): The trip matrix.
        dist_matrices (dict of np.ndarray or pd.DataFrame): Dictionary of distance matrices.
        alpha (float): The alpha coefficient.
        f_values (dict of float, optional): Dictionary of f coefficients for each matrix. If None, defaults to 0 for each matrix.

    Returns:
        dict: Normalized matrices.
    """

    # Default f_values to 0 for each key in dist_matrices if not provided
    if not f_values:
        f_values = {key: 0 for key in dist_matrices.keys()}

    exp_matrices = {}
    for key, matrix in dist_matrices.items():
        f = f_values.get(key, 0)

        # Convert DataFrame to numpy array if needed
        if isinstance(matrix, pd.DataFrame):
            matrix = matrix.values

        exp_matrix = np.exp(-1 * (matrix * alpha + f))
        exp_matrices[key] = exp_matrix

    # Calculate the sum of all exponentials
    sum_exp = sum(exp_matrices.values())

    # Normalize each matrix & multiply by trip matrix and update the matrices
    normalized_matrices = {key: (exp_matrix / sum_exp) * trip_matrix for key, exp_matrix in exp_matrices.items()}

    return normalized_matrices


def redistributeModalShares(dist_matrices, trip_matrices, redistribution_rules, threshold=0.5):
    """
    Redistribute trips among mobility networks based on given redistribution rules and when travel times are within a specified threshold.

    Args:
        dist_matrices (dict): Dictionary of distance matrices (travel times) for different mobility networks, keyed by identifier.
        trip_matrices (dict): Dictionary of matrices representing the number of trips for each mobility network, keyed by identifier.
        redistribution_rules (list): List of redistribution rules with "from" and "to" network identifiers.
        threshold (float): The threshold for considering travel times as similar.

    Returns:
        dict: Updated dictionary of trip matrices with transferred trips.
    """

    # Verify that all specified matrices exist in the input dictionaries
    for rule in redistribution_rules:
        if rule["from"] not in dist_matrices or rule["from"] not in trip_matrices:
            raise ValueError(f"Matrix ID {rule['from']} not found in the inputs.")
        for to_id in rule["to"]:
            if to_id not in dist_matrices or to_id not in trip_matrices:
                raise ValueError(f"Matrix ID {to_id} not found in the inputs.")

    # Copy the trip_matrices to avoid modifying the input directly
    updated_trip_matrices = {k: v.copy() for k, v in trip_matrices.items()}

    # Redistribute trips based on the rules and the threshold
    for rule in redistribution_rules:
        from_matrix_id = rule["from"]
        from_matrix_trips = updated_trip_matrices[from_matrix_id]
        from_matrix_dist = dist_matrices[from_matrix_id]

        for to_matrix_id in rule["to"]:
            to_matrix_dist = dist_matrices[to_matrix_id]

            # Create a boolean array where the absolute difference in travel times is less than or equal to the threshold
            similar_travel_time = np.abs(from_matrix_dist - to_matrix_dist) <= threshold

            # Find the indices where there are trips to transfer under the new condition
            indices_to_transfer = similar_travel_time & (from_matrix_trips > 0)

            # Transfer trips where the condition is True
            updated_trip_matrices[to_matrix_id][indices_to_transfer] += from_matrix_trips[indices_to_transfer]

            # Zero out the transferred trips in the from_matrix
            from_matrix_trips[indices_to_transfer] = 0

    # Return the updated trip matrices dictionary
    return updated_trip_matrices



def computeDistanceBrackets(trip_matrices, metric_dist_matrices, dist_brackets=[800, 2400, 4800]):
    # Transform the keys of metric_dist_matrices to match with trip_matrices
    transformed_metric_keys = {key.replace("metric_matrix", "distance_matrix")+"_noEntr": matrix
                               for key, matrix in metric_dist_matrices.items()}

    # Initialize dictionary to store aggregated trips per distance bracket
    bracket_totals = {bracket: 0 for bracket in dist_brackets}

    # Iterate over each pair of trip matrix and distance matrix
    for key, trip_matrix in trip_matrices.items():
        # Find the corresponding distance matrix
        dist_matrix = transformed_metric_keys.get(key)
        if dist_matrix is None:
            print("no matrxi found")
            continue  # Skip if no corresponding distance matrix found

        # Calculate trips for each distance bracket
        for i, bracket in enumerate(dist_brackets):
            if i == 0:
                # For the first bracket, count trips with distance <= bracket
                bracket_totals[bracket] += (trip_matrix[dist_matrix <= bracket]).sum().sum()
            else:
                # For subsequent brackets, count trips within the bracket range
                prev_bracket = dist_brackets[i - 1]
                bracket_totals[bracket] += (trip_matrix[(dist_matrix > prev_bracket) & (dist_matrix <= bracket)]).sum().sum()
    brackets_sum = sum(bracket_totals.values())
    brackets_rel = {str(bracket): round(total / brackets_sum, 3) for bracket, total in bracket_totals.items()}
    return brackets_rel


def computeTripStats(trip_matrices, distance_matrices, metric_dist_matrices, pop):
    # Transform the keys of metric_dist_matrices to match with trip_matrices
    transformed_metric_keys = {key.replace("metric_matrix", "distance_matrix")+"_noEntr": matrix
                               for key, matrix in metric_dist_matrices.items()}

    trips = 0
    totalTravelDistance = 0
    totalTravelTime = 0
    # Iterate over each pair of trip matrix and distance matrix
    for key, trip_matrix in trip_matrices.items():
        # Find the corresponding distance matrix
        metric_dist_matrix = transformed_metric_keys.get(key)
        dist_matrix = distance_matrices.get(key)
        if metric_dist_matrix is None:
            print("no matrxi found")
            continue  # Skip if no corresponding distance matrix found

        # compute
        totalTravelTime += (dist_matrix*trip_matrix).sum().sum()
        trips +=  trip_matrix.sum().sum()
        totalTravelDistance += (metric_dist_matrix*trip_matrix).sum().sum()

    MeanTripDistance = totalTravelDistance/trips
    MeanTravelDistancePerPerson = totalTravelDistance/pop

    MeanTravelTime = totalTravelTime/trips
    MeanTravelTimePerPerson = totalTravelTime/pop

    return totalTravelDistance, totalTravelTime, MeanTripDistance, MeanTravelDistancePerPerson, MeanTravelTime, MeanTravelTimePerPerson

def calculate_relative_mode_share(trip_matrices):
    """
    Calculate the relative mode share for a dictionary of trip matrices.

    Args:
        trip_matrices (dict of np.ndarray or pd.DataFrame): Dictionary of trip matrices.

    Returns:
        dict: Relative mode distribution for each key in trip_matrices.
    """

    # Compute the total trips for each mode
    total_trips_per_mode = {key: matrix.sum().sum() for key, matrix in trip_matrices.items()}

    # Compute the total trips across all modes
    total_trips_all_modes = sum(total_trips_per_mode.values())

    # Calculate the relative mode distribution
    rel_mode_distribution = {key: trips_per_mode / total_trips_all_modes for key, trips_per_mode in total_trips_per_mode.items()}

    return rel_mode_distribution


def extract_distance_matrices(stream, distance_matrices_of_interest):
    """
    Extract distance matrices from the stream and convert them to pandas DataFrames.
    Args:
        stream (dict): Stream data containing distance matrices.
        distance_matrices_of_interest (list of str): List of keys for the distance matrices of interest.
    Returns:
        dict: A dictionary of pandas DataFrames, where each key is a distance matrix kind.
    """
    distance_matrices = {}
    for distMK in distance_matrices_of_interest:
        for distM in stream["@Data"]['@{0}']:
            #print( distM.__dict__.keys())
            try:
                distMdict = distM.__dict__[distMK]

                distance_matrix_dict = json.loads(distMdict)
                origin_ids = distance_matrix_dict["origin_uuid"]
                destination_ids = distance_matrix_dict["destination_uuid"]
                distance_matrix = distance_matrix_dict["matrix"]

                # Convert the distance matrix to a DataFrame
                df_distances = pd.DataFrame(distance_matrix, index=origin_ids, columns=destination_ids)
                distance_matrices[distMK] = df_distances
            except Exception as e:
                pass
    return distance_matrices
#==================================================================================================



def computeTrips(
    df_distributionMatrix,
    df_total_trips_allNodes,
    df_distMatrix_speckle,
    df_alphas,
    df_attraction_proNode_sum_total,
    df_distances_aligned,
    TARGET_TRIP_RATE,
    SCALING_FACTOR,
    total_population,
    tot_res,
    tot_vis,
    

    distance_matrices,
    metric_matrices,
    redistributeTrips,
    DISTANCE_BRACKETS,

    alpha_low, alpha_med, alpha_high,
    alpha_mode,
    alpha_uniform,
    NEW_F_VALUES,
    
    CLIENT, 
    TARGET_STREAM, 
    TARGET_BRANCH,
    sourceInfo="", 
    ):

    NEW_ALPHAS = reconstruct_dataframe(alpha_low, alpha_med, alpha_high, df_alphas)
    NEW_MODE_ALPHA = alpha_mode


    # ====
    #step 1
    distributionMatrix_step1M = step_1(df_distributionMatrix,
                                    df_total_trips_allNodes)

    #step 2
    df_step_2M = step_2_vectorized(df_distMatrix_speckle,
                                NEW_ALPHAS)




    #step 3
    distAndAreaBasedAttr_step3M, distAndAreaBasedAttr_step3_sumM  = step_3_numba(df_attraction_proNode_sum_total,
                                                                            df_step_2M)


    #step 4
    distAndAreaBasedAttr_step4M, distAndAreaBasedAttr_step4_sumM = step_4_numba(distAndAreaBasedAttr_step3M,
                                                                            distAndAreaBasedAttr_step3_sumM)



    #step 5
    df_trips_proNode_proActivity_totalM = step_5_numba(distributionMatrix_step1M,
                                                    distAndAreaBasedAttr_step4M)

    #step 6 & 7
    df_tripMatrixM = step_6_7_vectorized(df_trips_proNode_proActivity_totalM)


    #step 8
    df_tripMatrix_adjustedM, df_tripMatrix_adjusted_scaledM = adjTripRate_adjFactor(df_tripMatrixM,
                                                                                total_population,
                                                                                TARGET_TRIP_RATE,
                                                                                SCALING_FACTOR )
    #------
    #MAIN 1 compute trip matrice per mode
    trip_matricesM = computeModalShare(df_tripMatrix_adjusted_scaledM,
                                    distance_matrices,
                                    NEW_MODE_ALPHA,
                                    f_values=NEW_F_VALUES)

    #MAIN 2 compute modal shares (redistribute trips in case of identical travel time)
    trip_matrices_redisM = redistributeModalShares(distance_matrices,
                                                trip_matricesM,
                                                redistributeTrips)

    #POST 1 compute mode shares
    rel_mode_distributionM = calculate_relative_mode_share(trip_matrices_redisM)


    #POST 2 distance brackets
    dist_sharesM = computeDistanceBrackets(trip_matrices_redisM,
                                        metric_matrices,
                                        DISTANCE_BRACKETS)

    #POST 3 compute more stats
    (totalTravelDistance, totalTravelTime,
    MeanTripDistance,MeanTravelDistancePerPerson,
    MeanTripTime, MeanTravelTimePerPerson) = computeTripStats(trip_matrices_redisM,
                                                            distance_matrices,
                                                            metric_matrices,
                                                            total_population)


    uniform_tripmatrix = getUniformMatrix(df_distances_aligned, df_tripMatrix_adjustedM, alpha_uniform)

    #add to dataframe
    # Define your parameter and target values
    newdata = {
        # Model Parameter==

        # Alpha - Routing
        "alpha_low": alpha_low,
        "alpha_med": alpha_med,
        "alpha_high": alpha_high,
        "alpha_uniform":alpha_uniform,

        "fvalues":NEW_F_VALUES,


        "alpha_mode":NEW_MODE_ALPHA,

        # Model Indicators ==

        # Modal Shares
        "share_ped_mm_art": rel_mode_distributionM['activity_node+distance_matrix_ped_mm_art_noEntr'],
        "share_ped_mm": rel_mode_distributionM['activity_node+distance_matrix_ped_mm_noEntr'],
        "share_ped": rel_mode_distributionM['activity_node+distance_matrix_ped_noEntr'],
        "share_ped_art": rel_mode_distributionM['activity_node+distance_matrix_ped_art_noEntr'],

        # Tripshares by Distance Brackets
        "800": dist_sharesM["800"],
        "2400": dist_sharesM["2400"],
        "4800": dist_sharesM["4800"],

        # Travel Time & Distances
        "totalTravelDistance":totalTravelDistance,
        "totalTravelTime":totalTravelTime,
        "MeanTravelTimePerPerson":MeanTravelTimePerPerson,

        # Trip Distances
        "MeanTripDistance":MeanTripDistance,
        "MeanTripTime":MeanTripTime,
        "MeanTravelDistancePerPerson":MeanTravelDistancePerPerson,

    }



    trip_matrice_adjName = {k.replace("distance", "trip"):v for k, v in trip_matricesM.items()}
    trip_matrice_adjName["tripMatrix_landuse"] = df_tripMatrix_adjusted_scaledM
    trip_matrice_adjName["tripMatrix_uniform"] = uniform_tripmatrix

    extraData = {"population":total_population,
        "residents":tot_res,
        "visitors":tot_vis,
        "parameter":newdata,
        }
    
    commitMsg = "automatic update"
    try:
        commitMsg += " using these commits: #+ "
        for k,v in sourceInfo.items():
            commitMsg += f" {k}: {v}"
    except:
        pass
    print(commitMsg)

    commit_id = send_matrices_and_create_commit(
            trip_matrice_adjName,
            CLIENT, 
            TARGET_STREAM, 
            TARGET_BRANCH, 
            commitMsg,
            rows_per_chunk=300,
            containerMetadata=extraData
        )
    print ("===============================")
    return newdata


#==================================================================================================
# speckle send 

def send_row_bundle(rows, indices, transport):
    bundle_object = Base()
    bundle_object.rows = rows
    bundle_object.indices = indices
    bundle_id = operations.send(base=bundle_object, transports=[transport])
    return bundle_id

def send_matrix(matrix_df, transport, rows_per_chunk):
    matrix_object = Base(metaData="Some metadata")
    batch_index = 0  # Maintain a separate counter for batch indexing

    # Bundle rows together
    rows = []
    indices = []
    for index, row in matrix_df.iterrows():
        rows.append([round(r,4) for r in row.tolist()])
        indices.append(str(index))
        if len(rows) == rows_per_chunk:
            bundle_id = send_row_bundle(rows, indices, transport)
            # Set the reference to the bundle in the matrix object using setattr
            setattr(matrix_object, f"@batch_{batch_index}", {"referencedId": bundle_id})
            rows, indices = [], []  # Reset for the next bundle
            batch_index += 1  # Increment the batch index
            print( str(rows_per_chunk) +" rows has been sent")

    # Don't forget to send the last bundle if it's not empty
    if rows:
        bundle_id = send_row_bundle(rows, indices, transport)
        setattr(matrix_object, f"@batch_{batch_index}", {"referencedId": bundle_id})

    # Send the matrix object to Speckle
    matrix_object_id = operations.send(base=matrix_object, transports=[transport])
    return matrix_object_id





# Main function to send all matrices and create a commit
def send_matrices_and_create_commit(matrices, client, stream_id, branch_name, commit_message, rows_per_chunk, containerMetadata):
    transport = ServerTransport(client=client, stream_id=stream_id)
    matrix_ids = {}

    # Send each matrix row by row and store its object ID
    for k, df in matrices.items():
        matrix_ids[k] = send_matrix(df, transport, rows_per_chunk)
        print("object: " + k + " has been sent")

    # Create a container object that will hold references to all the matrix objects
    container_object = Base()

    for k, v in containerMetadata.items():
      container_object[k] = v

    # Assuming you have a way to reference matrix objects by their IDs in Speckle
    for k, obj_id in matrix_ids.items():
      print("obj_id", obj_id)
      container_object[k] = obj_id

    
    # Dynamically add references to the container object
    for matrix_name, matrix_id in matrix_ids.items():
        # This assigns a reference to the matrix object by its ID
        # You might need to adjust this based on how your Speckle server expects to receive references
        setattr(container_object, matrix_name, {"referencedId": matrix_id})
        


    # Send the container object
    container_id = operations.send(base=container_object, transports=[transport])


    # Now use the container_id when creating the commit
    commit_id = client.commit.create(
        stream_id=stream_id,
        object_id=container_id,  # Use the container's ID here
        branch_name=branch_name,
        message=commit_message,
      )