-
-
Notifications
You must be signed in to change notification settings - Fork 252
Expand file tree
/
Copy pathfunction.py
More file actions
2849 lines (2584 loc) · 112 KB
/
function.py
File metadata and controls
2849 lines (2584 loc) · 112 KB
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
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
from inspect import signature
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
from scipy import integrate, linalg, optimize
try:
from functools import cached_property
except ImportError:
from ..tools import cached_property
class Function:
"""Class converts a python function or a data sequence into an object
which can be handled more naturally, enabling easy interpolation,
extrapolation, plotting and algebra.
"""
def __init__(
self,
source,
inputs=["Scalar"],
outputs=["Scalar"],
interpolation=None,
extrapolation=None,
title=None,
):
"""Convert source into a Function, to be used more naturally.
Set inputs, outputs, domain dimension, interpolation and extrapolation
method, and process the source.
Parameters
----------
source : function, scalar, ndarray, string
The actual function. If type is function, it will be called for
evaluation. If type is int or float, it will be treated as a
constant function. If ndarray, its points will be used for
interpolation. An ndarray should be as [(x0, y0, z0), (x1, y1, z1),
(x2, y2, z2), ...] where x0 and y0 are inputs and z0 is output. If
string, imports file named by the string and treats it as csv.
The file is converted into ndarray and should not have headers.
inputs : string, sequence of strings, optional
The name of the inputs of the function. Will be used for
representation and graphing (axis names). 'Scalar' is default.
If source is function, int or float and has multiple inputs,
this parameter must be given for correct operation.
outputs : string, sequence of strings, optional
The name of the outputs of the function. Will be used for
representation and graphing (axis names). Scalar is default.
interpolation : string, optional
Interpolation method to be used if source type is ndarray.
For 1-D functions, linear, polynomial, akima and spline are
supported. For N-D functions, only shepard is supported.
Default for 1-D functions is spline.
extrapolation : string, optional
Extrapolation method to be used if source type is ndarray.
Options are 'natural', which keeps interpolation, 'constant',
which returns the value of the function at the edge of the interval,
and 'zero', which returns zero for all points outside of source
range. Default for 1-D functions is constant.
title : string, optional
Title to be displayed in the plots' figures. If none, the title will
be constructed using the inputs and outputs arguments in the form
of "{inputs} x {outputs}".
Returns
-------
None
"""
# Set input and output
self.set_inputs(inputs)
self.set_outputs(outputs)
# Save interpolation method
self.__interpolation__ = interpolation
self.__extrapolation__ = extrapolation
# Initialize last_interval
self.last_interval = 0
# Set source
self.set_source(source)
# Set function title
self.set_title(title)
# Return
return None
# Define all set methods
def set_inputs(self, inputs):
"""Set the name and number of the incoming arguments of the Function.
Parameters
----------
inputs : string, sequence of strings
The name of the parameters (inputs) of the Function.
Returns
-------
self : Function
"""
self.__inputs__ = [inputs] if isinstance(inputs, str) else list(inputs)
self.__dom_dim__ = len(self.__inputs__)
return self
def set_outputs(self, outputs):
"""Set the name and number of the output of the Function.
Parameters
----------
outputs : string, sequence of strings
The name of the output of the function. Example: Distance (m).
Returns
-------
self : Function
"""
self.__outputs__ = [outputs] if isinstance(outputs, str) else list(outputs)
self.__img_dim__ = len(self.__outputs__)
return self
def set_source(self, source):
"""Set the source which defines the output of the function giving a
certain input.
Parameters
----------
source : function, scalar, ndarray, string, Function
The actual function. If type is function, it will be called for
evaluation. If type is int or float, it will be treated as a
constant function. If ndarray, its points will be used for
interpolation. An ndarray should be as [(x0, y0, z0), (x1, y1, z1),
(x2, y2, z2), ...] where x0 and y0 are inputs and z0 is output. If
string, imports file named by the string and treats it as csv.
The file is converted into ndarray and should not have headers.
If the source is a Function, its source will be copied and another
Function will be created following the new inputs and outputs.
Returns
-------
self : Function
"""
# If the source is a Function
if isinstance(source, Function):
source = source.get_source()
# Import CSV if source is a string or Path and convert values to ndarray
if isinstance(source, (str, Path)):
# Read file and check for headers
f = open(source, "r")
first_line = f.readline()
# If headers are found...
if first_line[0] in ['"', "'"]:
# Headers available
first_line = first_line.replace('"', " ").replace("'", " ")
first_line = first_line.split(" , ")
self.set_inputs(first_line[0])
self.set_outputs(first_line[1:])
source = np.loadtxt(source, delimiter=",", skiprows=1, dtype=float)
# if headers are not found
else:
source = np.loadtxt(source, delimiter=",", dtype=float)
# Convert to ndarray if source is a list
if isinstance(source, (list, tuple)):
source = np.array(source, dtype=np.float64)
# Convert number source into vectorized lambda function
if isinstance(source, (int, float)):
temp = 1 * source
def source(x):
return temp
# Handle callable source or number source
if callable(source):
# Set source
self.source = source
# Set get_value_opt
self.get_value_opt = source
# Set arguments name and domain dimensions
parameters = signature(source).parameters
self.__dom_dim__ = len(parameters)
if self.__inputs__ == ["Scalar"]:
self.__inputs__ = list(parameters)
# Set interpolation and extrapolation
self.__interpolation__ = None
self.__extrapolation__ = None
# Handle ndarray source
else:
# Check to see if dimensions match incoming data set
new_total_dim = len(source[0, :])
old_total_dim = self.__dom_dim__ + self.__img_dim__
dV = self.__inputs__ == ["Scalar"] and self.__outputs__ == ["Scalar"]
# If they don't, update default values or throw error
if new_total_dim != old_total_dim:
if dV:
# Update dimensions and inputs
self.__dom_dim__ = new_total_dim - 1
self.__inputs__ = self.__dom_dim__ * self.__inputs__
else:
# User has made a mistake inputting inputs and outputs
print("Error in input and output dimensions!")
return None
# Do things if domDim is 1
if self.__dom_dim__ == 1:
source = source[source[:, 0].argsort()]
self.x_array = source[:, 0]
self.xinitial, self.xfinal = self.x_array[0], self.x_array[-1]
self.y_array = source[:, 1]
self.y_initial, self.y_final = self.y_array[0], self.y_array[-1]
# Finally set data source as source
self.source = source
# Update extrapolation method
if self.__extrapolation__ is None:
self.set_extrapolation()
# Set default interpolation for point source if it hasn't
if self.__interpolation__ is None:
self.set_interpolation()
else:
# Updates interpolation coefficients
self.set_interpolation(self.__interpolation__)
# Do things if function is multivariate
else:
self.x_array = source[:, 0]
self.xinitial, self.xfinal = self.x_array[0], self.x_array[-1]
self.y_array = source[:, 1]
self.y_initial, self.y_final = self.y_array[0], self.y_array[-1]
self.z_array = source[:, 2]
self.z_initial, self.z_final = self.z_array[0], self.z_array[-1]
# Finally set data source as source
self.source = source
if self.__interpolation__ is None:
self.set_interpolation("shepard")
# Return self
return self
@cached_property
def min(self):
"""Get the minimum value of the Function y_array.
Raises an error if the Function is lambda based.
Returns
-------
minimum : float
"""
return self.y_array.min()
@cached_property
def max(self):
"""Get the maximum value of the Function y_array.
Raises an error if the Function is lambda based.
Returns
-------
maximum : float
"""
return self.y_array.max()
def set_interpolation(self, method="spline"):
"""Set interpolation method and process data is method requires.
Parameters
----------
method : string, optional
Interpolation method to be used if source type is ndarray.
For 1-D functions, linear, polynomial, akima and spline is
supported. For N-D functions, only shepard is supported.
Default is 'spline'.
Returns
-------
self : Function
"""
# Set interpolation method
self.__interpolation__ = method
# Spline, akima and polynomial need data processing
# Shepard, and linear do not
if method == "spline":
self.__interpolate_spline__()
elif method == "polynomial":
self.__interpolate_polynomial__()
elif method == "akima":
self.__interpolate_akima__()
# Set get_value_opt
self.set_get_value_opt()
# Returns self
return self
def set_extrapolation(self, method="constant"):
"""Set extrapolation behavior of data set.
Parameters
----------
extrapolation : string, optional
Extrapolation method to be used if source type is ndarray.
Options are 'natural', which keeps interpolation, 'constant',
which returns the value of the function at the edge of the interval,
and 'zero', which returns zero for all points outside of source
range. Default is 'constant'.
Returns
-------
self : Function
"""
# Set extrapolation method
self.__extrapolation__ = method
# Return self
return self
def set_get_value_opt(self):
"""Crates a method that evaluates interpolations rather quickly
when compared to other options available, such as just calling
the object instance or calling ``Function.get_value directly``. See
``Function.get_value_opt`` for documentation.
Returns
-------
self : Function
"""
# Retrieve general info
x_data = self.x_array
y_data = self.y_array
x_min, x_max = self.xinitial, self.xfinal
if self.__extrapolation__ == "zero":
extrapolation = 0 # Extrapolation is zero
elif self.__extrapolation__ == "natural":
extrapolation = 1 # Extrapolation is natural
elif self.__extrapolation__ == "constant":
extrapolation = 2 # Extrapolation is constant
else:
raise ValueError(f"Invalid extrapolation type {self.__extrapolation__}")
# Crete method to interpolate this info for each interpolation type
if self.__interpolation__ == "spline":
coeffs = self.__spline_coefficients__
def get_value_opt(x):
x_interval = np.searchsorted(x_data, x)
# Interval found... interpolate... or extrapolate
if x_min <= x <= x_max:
# Interpolate
x_interval = x_interval if x_interval != 0 else 1
a = coeffs[:, x_interval - 1]
x = x - x_data[x_interval - 1]
y = a[3] * x**3 + a[2] * x**2 + a[1] * x + a[0]
else:
# Extrapolate
if extrapolation == 0: # Extrapolation == zero
y = 0
elif extrapolation == 1: # Extrapolation == natural
a = coeffs[:, 0] if x < x_min else coeffs[:, -1]
x = x - x_data[0] if x < x_min else x - x_data[-2]
y = a[3] * x**3 + a[2] * x**2 + a[1] * x + a[0]
else: # Extrapolation is set to constant
y = y_data[0] if x < x_min else y_data[-1]
return y
self.get_value_opt = get_value_opt
elif self.__interpolation__ == "linear":
def get_value_opt(x):
x_interval = np.searchsorted(x_data, x)
# Interval found... interpolate... or extrapolate
if x_min <= x <= x_max:
# Interpolate
dx = float(x_data[x_interval] - x_data[x_interval - 1])
dy = float(y_data[x_interval] - y_data[x_interval - 1])
y = (x - x_data[x_interval - 1]) * (dy / dx) + y_data[
x_interval - 1
]
else:
# Extrapolate
if extrapolation == 0: # Extrapolation == zero
y = 0
elif extrapolation == 1: # Extrapolation == natural
x_interval = 1 if x < x_min else -1
dx = float(x_data[x_interval] - x_data[x_interval - 1])
dy = float(y_data[x_interval] - y_data[x_interval - 1])
y = (x - x_data[x_interval - 1]) * (dy / dx) + y_data[
x_interval - 1
]
else: # Extrapolation is set to constant
y = y_data[0] if x < x_min else y_data[-1]
return y
self.get_value_opt = get_value_opt
elif self.__interpolation__ == "akima":
coeffs = np.array(self.__akima_coefficients__)
def get_value_opt(x):
x_interval = np.searchsorted(x_data, x)
# Interval found... interpolate... or extrapolate
if x_min <= x <= x_max:
# Interpolate
x_interval = x_interval if x_interval != 0 else 1
a = coeffs[4 * x_interval - 4 : 4 * x_interval]
y = a[3] * x**3 + a[2] * x**2 + a[1] * x + a[0]
else:
# Extrapolate
if extrapolation == 0: # Extrapolation == zero
y = 0
elif extrapolation == 1: # Extrapolation == natural
a = coeffs[:4] if x < x_min else coeffs[-4:]
y = a[3] * x**3 + a[2] * x**2 + a[1] * x + a[0]
else: # Extrapolation is set to constant
y = y_data[0] if x < x_min else y_data[-1]
return y
self.get_value_opt = get_value_opt
elif self.__interpolation__ == "polynomial":
coeffs = self.__polynomial_coefficients__
def get_value_opt(x):
# Interpolate... or extrapolate
if x_min <= x <= x_max:
# Interpolate
y = 0
for i in range(len(coeffs)):
y += coeffs[i] * (x**i)
else:
# Extrapolate
if extrapolation == 0: # Extrapolation == zero
y = 0
elif extrapolation == 1: # Extrapolation == natural
y = 0
for i in range(len(coeffs)):
y += coeffs[i] * (x**i)
else: # Extrapolation is set to constant
y = y_data[0] if x < x_min else y_data[-1]
return y
self.get_value_opt = get_value_opt
elif self.__interpolation__ == "shepard":
x_data = self.source[:, 0:-1] # Support for N-Dimensions
len_y_data = len(y_data) # A little speed up
def get_value_opt(*args):
x = np.array([[float(x) for x in list(args)]])
numerator_sum = 0
denominator_sum = 0
for i in range(len_y_data):
sub = x_data[i] - x
distance = np.linalg.norm(sub)
if distance == 0:
numerator_sum = y_data[i]
denominator_sum = 1
break
else:
weight = distance ** (-3)
numerator_sum = numerator_sum + y_data[i] * weight
denominator_sum = denominator_sum + weight
return numerator_sum / denominator_sum
self.get_value_opt = get_value_opt
# Returns self
return self
def set_discrete(
self,
lower=0,
upper=10,
samples=200,
interpolation="spline",
extrapolation="constant",
one_by_one=True,
):
"""This method transforms function defined Functions into list
defined Functions. It evaluates the function at certain points
(sampling range) and stores the results in a list, which is converted
into a Function and then returned. The original Function object is
replaced by the new one.
Parameters
----------
lower : scalar, optional
Value where sampling range will start. Default is 0.
upper : scalar, optional
Value where sampling range will end. Default is 10.
samples : int, optional
Number of samples to be taken from inside range. Default is 200.
interpolation : string
Interpolation method to be used if source type is ndarray.
For 1-D functions, linear, polynomial, akima and spline is
supported. For N-D functions, only shepard is supported.
Default is 'spline'.
extrapolation : string, optional
Extrapolation method to be used if source type is ndarray.
Options are 'natural', which keeps interpolation, 'constant',
which returns the value of the function at the edge of the interval,
and 'zero', which returns zero for all points outside of source
range. Default is 'constant'.
one_by_one : boolean, optional
If True, evaluate Function in each sample point separately. If
False, evaluates Function in vectorized form. Default is True.
Returns
-------
self : Function
"""
if self.__dom_dim__ == 1:
xs = np.linspace(lower, upper, samples)
ys = self.get_value(xs.tolist()) if one_by_one else self.get_value(xs)
self.set_source(np.concatenate(([xs], [ys])).transpose())
self.set_interpolation(interpolation)
self.set_extrapolation(extrapolation)
elif self.__dom_dim__ == 2:
lower = 2 * [lower] if isinstance(lower, (int, float)) else lower
upper = 2 * [upper] if isinstance(upper, (int, float)) else upper
sam = 2 * [samples] if isinstance(samples, (int, float)) else samples
# Create nodes to evaluate function
xs = np.linspace(lower[0], upper[0], sam[0])
ys = np.linspace(lower[1], upper[1], sam[1])
xs, ys = np.meshgrid(xs, ys)
xs, ys = xs.flatten(), ys.flatten()
mesh = [[xs[i], ys[i]] for i in range(len(xs))]
# Evaluate function at all mesh nodes and convert it to matrix
zs = np.array(self.get_value(mesh))
self.set_source(np.concatenate(([xs], [ys], [zs])).transpose())
self.__interpolation__ = "shepard"
return self
def set_discrete_based_on_model(
self, model_function, one_by_one=True, keep_self=True
):
"""This method transforms the domain of Function instance into a list of
discrete points based on the domain of a model Function instance. It
does so by retrieving the domain, domain name, interpolation method and
extrapolation method of the model Function instance. It then evaluates
the original Function instance in all points of the retrieved domain to
generate the list of discrete points that will be used for interpolation
when this Function is called.
Parameters
----------
model_function : Function
Function object that will be used to define the sampling points,
interpolation method and extrapolation method.
Must be a Function whose source attribute is a list (i.e. a list
based Function instance). Must have the same domain dimension as the
Function to be discretized.
one_by_one : boolean, optional
If True, evaluate Function in each sample point separately. If
False, evaluates Function in vectorized form. Default is True.
keepSelf : boolean, optional
If True, the original Function interpolation and extrapolation
methods will be kept. If False, those are substituted by the ones
from the model Function. Default is True.
Returns
-------
self : Function
See also
--------
Function.set_discrete
Examples
--------
This method is particularly useful when algebraic operations are carried
out using Function instances defined by different discretized domains
(same range, but different mesh size). Once an algebraic operation is
done, it will not directly be applied between the list of discrete
points of the two Function instances. Instead, the result will be a
Function instance defined by a callable that calls both Function
instances and performs the operation. This makes the evaluation of the
resulting Function inefficient, due to extra function calling overhead
and multiple interpolations being carried out.
>>> from rocketpy import Function
>>> f = Function([(0, 0), (1, 1), (2, 4), (3, 9), (4, 16)])
>>> g = Function([(0, 0), (2, 2), (4, 4)])
>>> h = f * g
>>> type(h.source)
<class 'function'>
Therefore, it is good practice to make sure both Function instances are
defined by the same domain, i.e. by the same list of mesh points. This
way, the algebraic operation will be carried out directly between the
lists of discrete points, generating a new Function instance defined by
this result. When it is evaluated, there are no extra function calling
overheads neither multiple interpolations.
>>> g.set_discrete_based_on_model(f)
'Function from R1 to R1 : (Scalar) → (Scalar)'
>>> h = f * g
>>> h.source
array([[ 0., 0.],
[ 1., 1.],
[ 2., 8.],
[ 3., 27.],
[ 4., 64.]])
Notes
-----
1. This method performs in place replacement of the original Function
object source.
2. This method is similar to set_discrete, but it uses the domain of a
model Function to define the domain of the new Function instance.
"""
if not isinstance(model_function.source, np.ndarray):
raise TypeError("model_function must be a list based Function.")
if model_function.__dom_dim__ != self.__dom_dim__:
raise ValueError("model_function must have the same domain dimension.")
if self.__dom_dim__ == 1:
xs = model_function.source[:, 0]
ys = self.get_value(xs.tolist()) if one_by_one else self.get_value(xs)
self.set_source(np.concatenate(([xs], [ys])).transpose())
elif self.__dom_dim__ == 2:
# Create nodes to evaluate function
xs = model_function.source[:, 0]
ys = model_function.source[:, 1]
xs, ys = np.meshgrid(xs, ys)
xs, ys = xs.flatten(), ys.flatten()
mesh = [[xs[i], ys[i]] for i in range(len(xs))]
# Evaluate function at all mesh nodes and convert it to matrix
zs = np.array(self.get_value(mesh))
self.set_source(np.concatenate(([xs], [ys], [zs])).transpose())
interp = (
self.__interpolation__ if keep_self else model_function.__interpolation__
)
extrap = (
self.__extrapolation__ if keep_self else model_function.__extrapolation__
)
self.set_interpolation(interp)
self.set_extrapolation(extrap)
return self
def reset(
self,
inputs=None,
outputs=None,
interpolation=None,
extrapolation=None,
title=None,
):
"""This method allows the user to reset the inputs, outputs,
interpolation and extrapolation settings of a Function object, all at
once, without having to call each of the corresponding methods.
Parameters
----------
inputs : string, sequence of strings, optional
List of input variable names. If None, the original inputs are kept.
See Function.set_inputs for more information.
outputs : string, sequence of strings, optional
List of output variable names. If None, the original outputs are
kept. See Function.set_outputs for more information.
interpolation : string, optional
Interpolation method to be used if source type is ndarray.
See Function.set_interpolation for more information.
extrapolation : string, optional
Extrapolation method to be used if source type is ndarray.
See Function.set_extrapolation for more information.
Examples
--------
A simple use case is to reset the inputs and outputs of a Function
object that has been defined by algebraic manipulation of other Function
objects.
>>> from rocketpy import Function
>>> v = Function(lambda t: (9.8*t**2)/2, inputs='t', outputs='v')
>>> mass = 10 # Mass
>>> kinetic_energy = mass * v**2 / 2
>>> v.get_inputs(), v.get_outputs()
(['t'], ['v'])
>>> kinetic_energy
'Function from R1 to R1 : (x) → (Scalar)'
>>> kinetic_energy.reset(inputs='t', outputs='Kinetic Energy');
'Function from R1 to R1 : (t) → (Kinetic Energy)'
Returns
-------
self : Function
"""
if inputs is not None:
self.set_inputs(inputs)
if outputs is not None:
self.set_outputs(outputs)
if interpolation is not None and interpolation != self.__interpolation__:
self.set_interpolation(interpolation)
if extrapolation is not None and extrapolation != self.__extrapolation__:
self.set_extrapolation(extrapolation)
self.set_title(title)
return self
# Define all get methods
def get_inputs(self):
"Return tuple of inputs of the function."
return self.__inputs__
def get_outputs(self):
"Return tuple of outputs of the function."
return self.__outputs__
def get_source(self):
"Return source list or function of the Function."
return self.source
def get_image_dim(self):
"Return int describing dimension of the image space of the function."
return self.__img_dim__
def get_domain_dim(self):
"Return int describing dimension of the domain space of the function."
return self.__dom_dim__
def get_interpolation_method(self):
"Return string describing interpolation method used."
return self.__interpolation__
def get_extrapolation_method(self):
"Return string describing extrapolation method used."
return self.__extrapolation__
def get_value(self, *args):
"""This method returns the value of the Function at the specified
point. See Function.get_value_opt for a faster, but limited,
implementation.
Parameters
----------
args : scalar, list
Value where the Function is to be evaluated. If the Function is
1-D, only one argument is expected, which may be an int, a float
or a list of ints or floats, in which case the Function will be
evaluated at all points in the list and a list of floats will be
returned. If the function is N-D, N arguments must be given, each
one being an scalar or list.
Returns
-------
ans : scalar, list
"""
# Return value for Function of function type
if callable(self.source):
if len(args) == 1 and isinstance(args[0], (list, tuple)):
if isinstance(args[0][0], (tuple, list)):
return [self.source(*arg) for arg in args[0]]
else:
return [self.source(arg) for arg in args[0]]
elif len(args) == 1 and isinstance(args[0], np.ndarray):
return self.source(args[0])
else:
return self.source(*args)
# Returns value for shepard interpolation
elif self.__interpolation__ == "shepard":
if isinstance(args[0], (list, tuple)):
x = list(args[0])
else:
x = [[float(x) for x in list(args)]]
ans = x
x_data = self.source[:, 0:-1]
y_data = self.source[:, -1]
for i in range(len(x)):
numerator_sum = 0
denominator_sum = 0
for o in range(len(y_data)):
sub = x_data[o] - x[i]
distance = (sub.dot(sub)) ** (0.5)
# print(x_data[o], x[i], distance)
if distance == 0:
numerator_sum = y_data[o]
denominator_sum = 1
break
else:
weight = distance ** (-3)
numerator_sum = numerator_sum + y_data[o] * weight
denominator_sum = denominator_sum + weight
ans[i] = numerator_sum / denominator_sum
return ans if len(ans) > 1 else ans[0]
# Returns value for polynomial interpolation function type
elif self.__interpolation__ == "polynomial":
if isinstance(args[0], (int, float)):
args = [list(args)]
x = np.array(args[0])
x_data = self.x_array
y_data = self.y_array
x_min, x_max = self.xinitial, self.xfinal
coeffs = self.__polynomial_coefficients__
A = np.zeros((len(args[0]), coeffs.shape[0]))
for i in range(coeffs.shape[0]):
A[:, i] = x**i
ans = A.dot(coeffs).tolist()
for i in range(len(x)):
if not (x_min <= x[i] <= x_max):
if self.__extrapolation__ == "constant":
ans[i] = y_data[0] if x[i] < x_min else y_data[-1]
elif self.__extrapolation__ == "zero":
ans[i] = 0
return ans if len(ans) > 1 else ans[0]
# Returns value for spline, akima or linear interpolation function type
elif self.__interpolation__ in ["spline", "akima", "linear"]:
if isinstance(args[0], (int, float, complex, np.integer)):
args = [list(args)]
x = [arg for arg in args[0]]
x_data = self.x_array
y_data = self.y_array
x_intervals = np.searchsorted(x_data, x)
x_min, x_max = self.xinitial, self.xfinal
if self.__interpolation__ == "spline":
coeffs = self.__spline_coefficients__
for i in range(len(x)):
if x[i] == x_min or x[i] == x_max:
x[i] = y_data[x_intervals[i]]
elif x_min < x[i] < x_max or (self.__extrapolation__ == "natural"):
if not x_min < x[i] < x_max:
a = coeffs[:, 0] if x[i] < x_min else coeffs[:, -1]
x[i] = (
x[i] - x_data[0] if x[i] < x_min else x[i] - x_data[-2]
)
else:
a = coeffs[:, x_intervals[i] - 1]
x[i] = x[i] - x_data[x_intervals[i] - 1]
x[i] = a[3] * x[i] ** 3 + a[2] * x[i] ** 2 + a[1] * x[i] + a[0]
else:
# Extrapolate
if self.__extrapolation__ == "zero":
x[i] = 0
else: # Extrapolation is set to constant
x[i] = y_data[0] if x[i] < x_min else y_data[-1]
elif self.__interpolation__ == "linear":
for i in range(len(x)):
# Interval found... interpolate... or extrapolate
inter = x_intervals[i]
if x_min <= x[i] <= x_max:
# Interpolate
dx = float(x_data[inter] - x_data[inter - 1])
dy = float(y_data[inter] - y_data[inter - 1])
x[i] = (x[i] - x_data[inter - 1]) * (dy / dx) + y_data[
inter - 1
]
else:
# Extrapolate
if self.__extrapolation__ == "zero": # Extrapolation == zero
x[i] = 0
elif (
self.__extrapolation__ == "natural"
): # Extrapolation == natural
inter = 1 if x[i] < x_min else -1
dx = float(x_data[inter] - x_data[inter - 1])
dy = float(y_data[inter] - y_data[inter - 1])
x[i] = (x[i] - x_data[inter - 1]) * (dy / dx) + y_data[
inter - 1
]
else: # Extrapolation is set to constant
x[i] = y_data[0] if x[i] < x_min else y_data[-1]
else:
coeffs = self.__akima_coefficients__
for i in range(len(x)):
if x[i] == x_min or x[i] == x_max:
x[i] = y_data[x_intervals[i]]
elif x_min < x[i] < x_max or (self.__extrapolation__ == "natural"):
if not (x_min < x[i] < x_max):
a = coeffs[:4] if x[i] < x_min else coeffs[-4:]
else:
a = coeffs[4 * x_intervals[i] - 4 : 4 * x_intervals[i]]
x[i] = a[3] * x[i] ** 3 + a[2] * x[i] ** 2 + a[1] * x[i] + a[0]
else:
# Extrapolate
if self.__extrapolation__ == "zero":
x[i] = 0
else: # Extrapolation is set to constant
x[i] = y_data[0] if x[i] < x_min else y_data[-1]
if isinstance(args[0], np.ndarray):
return np.array(x)
else:
return x if len(x) > 1 else x[0]
def __getitem__(self, args):
"""Returns item of the Function source. If the source is not an array,
an error will result.
Parameters
----------
args : int, float
Index of the item to be retrieved.
Returns
-------
self.source[args] : float, array
Item specified from Function.source.
"""
return self.source[args]
def __len__(self):
"""Returns length of the Function source. If the source is not an
array, an error will result.
Returns
-------
len(self.source) : int
Length of Function.source.
"""
return len(self.source)
def __bool__(self):
"""Returns true if self exists. This is to avoid getting into __len__
method in boolean statements.
Returns
-------
bool : bool
Always True.
"""
return True
# Define all conversion methods
def to_frequency_domain(self, lower, upper, sampling_frequency, remove_dc=True):
"""Performs the conversion of the Function to the Frequency Domain and
returns the result. This is done by taking the Fourier transform of the
Function. The resulting frequency domain is symmetric, i.e., the
negative frequencies are included as well.
Parameters
----------
lower : float
Lower bound of the time range.
upper : float
Upper bound of the time range.
sampling_frequency : float
Sampling frequency at which to perform the Fourier transform.
remove_dc : bool, optional
If True, the DC component is removed from the Fourier transform.
Returns
-------
Function
The Function in the frequency domain.
Examples
--------
>>> from rocketpy import Function
>>> import numpy as np
>>> main_frequency = 10 # Hz
>>> time = np.linspace(0, 10, 1000)
>>> signal = np.sin(2 * np.pi * main_frequency * time)
>>> time_domain = Function(np.array([time, signal]).T)
>>> frequency_domain = time_domain.to_frequency_domain(lower=0, upper=10, sampling_frequency=100)
>>> peak_frequencies_index = np.where(frequency_domain[:, 1] > 0.001)
>>> peak_frequencies = frequency_domain[peak_frequencies_index, 0]
>>> print(peak_frequencies)
[[-10. 10.]]
"""
# Get the time domain data
sampling_time_step = 1.0 / sampling_frequency
sampling_range = np.arange(lower, upper, sampling_time_step)
number_of_samples = len(sampling_range)
sampled_points = self(sampling_range)
if remove_dc:
sampled_points -= np.mean(sampled_points)
fourier_amplitude = np.abs(np.fft.fft(sampled_points) / (number_of_samples / 2))
fourier_frequencies = np.fft.fftfreq(number_of_samples, sampling_time_step)
return Function(
source=np.array([fourier_frequencies, fourier_amplitude]).T,
inputs="Frequency (Hz)",
outputs="Amplitude",
interpolation="linear",
extrapolation="zero",
)
# Define all presentation methods
def __call__(self, *args):
"""Plot the Function if no argument is given. If an
argument is given, return the value of the function at the desired
point.
Parameters
----------
args : scalar, list, optional
Value where the Function is to be evaluated. If the Function is
1-D, only one argument is expected, which may be an int, a float
or a list of ints or floats, in which case the Function will be
evaluated at all points in the list and a list of floats will be
returned. If the function is N-D, N arguments must be given, each
one being an scalar or list.
Returns
-------
ans : None, scalar, list
"""
if len(args) == 0: