bibun 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'differential_butcher_table'
4
+
5
+ module Bibun
6
+ class DifferentialEquationSystem
7
+ # This class handles the inner process of numerical calculations to perform each integration step and set
8
+ # the changes to add to the variables to udpate them to the next step.
9
+ class DifferentialStepper
10
+ # @return [Bibun::DifferentialButcherTable] The butcher table with the info to perform the Runge-Kutta
11
+ # integration.
12
+ attr_accessor :butcher_table
13
+ # @return [Bibun::DifferentialEquationSystem] Reference to the outer DES object.
14
+ attr_accessor :system
15
+ # @return [Bibun::DifferentialEquationSystem::SymbolGroup] Reference to the SymbolGroup object of the outer DES
16
+ # object.
17
+ attr_accessor :syms
18
+ # @return [Bibun::DifferentialEquationSystem::StepTracker] Reference to the step tracker of the outer DES object.
19
+ attr_accessor :tracker
20
+ # @return [Array<Keisan::Calculator>] Array with the calculators for each formula of the interpolation (this is
21
+ # necessary to be able to cache the AST for each formula and improve performance).
22
+ attr_accessor :dense_calculators
23
+ # @return [Hash] A nested hash used to pass the values to log to the logger object.
24
+ attr_accessor :terms_log
25
+
26
+ # Basic initialization. +StepTracker+ and +GroupSymbol+ objects are assinged for easier access.
27
+ def initialize(parent_system, tracker, syms)
28
+ @system = parent_system
29
+ @tracker = tracker
30
+ @syms = syms
31
+ @butcher_table = DifferentialButcherTable.new
32
+ @adaptive = false
33
+ end
34
+
35
+ # Preparation prior the taking the first step.
36
+ def prepare(method: nil)
37
+ set_buffers
38
+ butcher_table.load_preset(method:) if butcher_table.empty?
39
+ return unless butcher_table.adaptive?
40
+
41
+ raise 'No tolerances provided for adaptive integration' unless syms.tolerances_provided?
42
+
43
+ set_interpolation if dense_output?
44
+ end
45
+
46
+ # Quick way to check if dense output has to be given.
47
+ # @return [Boolean]
48
+ def dense_output? = butcher_table.adaptive? && !tracker.logging_interval.nil?
49
+
50
+ # Step for explicit Runge-Kutta methods
51
+ def walk_step(step_size)
52
+ if butcher_table.adaptive?
53
+ adaptive_loop(step_size)
54
+ else
55
+ straightforward_step(step_size)
56
+ end
57
+ end
58
+
59
+ # Performs a single step, resulting in new values assigned to +DifferentialSystemVariable.step_size+
60
+ def straightforward_step(step_size, alternate_weights: false)
61
+ estimate_k_steps(step_size)
62
+ combine_with_weighted(step_size, butcher_table.weights)
63
+ combine_with_weighted(step_size, butcher_table.alternate_weights, alternate: true) if alternate_weights
64
+ end
65
+
66
+ # Loop for adaptive methods, which results in new values of +DifferentialSystemVariable.step_size+ and a
67
+ # new +StepTracker.step_size+
68
+ def adaptive_loop(step_size)
69
+ runner_step_size = step_size
70
+ acceptable_error = false
71
+ i = 0
72
+ until acceptable_error
73
+ i += 1
74
+ # First run
75
+ straightforward_step(runner_step_size, alternate_weights: true)
76
+ result = evaluate_error
77
+ runner_step_size *= result[:factor].clamp(1/5r, 10) # Suggestion by W.H. Press
78
+ if result[:err] <= 1
79
+ acceptable_error = true
80
+ else
81
+ # Perform again with lower step size, do not correct step_size again.
82
+ # Undo the change done from the previous, wrong step size.
83
+ syms.undo_term_changes
84
+ straightforward_step(runner_step_size, alternate_weights: true)
85
+ if evaluate_error[:err] <= 1
86
+ acceptable_error = true
87
+ else
88
+ # Adjust again step size for next iteration of the loop.
89
+ syms.undo_term_changes
90
+ runner_step_size *= result[:factor].clamp(1/5r, 10)
91
+ end
92
+ end
93
+ raise 'Too many adapations' if i >= 5
94
+ end
95
+ tracker.next_step_size = runner_step_size
96
+ end
97
+
98
+ # Provides to the logger the variable value that correspond to the current logging point.
99
+ # The leaps array and index are both provided to be able to assess the relative position of the index.
100
+ # @return [Numeric] The value to log for the variable.
101
+ def variable_log_value(variable, leaps, index)
102
+ if dense_output?
103
+ dense_interpolation(variable, leaps[index])
104
+ else
105
+ variable.value + variable.step_change
106
+ end
107
+ end
108
+
109
+ # Provides to the logger the rate additive term value that correspond to the current logging point.
110
+ # The leaps array and index are both provided to be able to assess the relative position of the index.
111
+ # @return [Numeric] The value to log for the rate additive term.
112
+ def term_log_value(term, marks, index)
113
+ if dense_output?
114
+ term_dense_accumulation(term, marks, index)
115
+ else
116
+ term.report_and_reset
117
+ end
118
+ end
119
+
120
+ # This routine records the amount of change that keeps unregistered at the end of each walking step and adds it
121
+ # to the first logging step of the next walking step. Also ensures only the change caused by each individual
122
+ # logging step is provided for those beyond the first.
123
+ # @return [Numeric] The net amount of change due to the rate additive term for dense interpolation.
124
+ def term_dense_accumulation(term, marks, index)
125
+ value = 0
126
+ interpol = dense_interpolation(term, marks[index])
127
+ value += interpol
128
+ if index == 0
129
+ value += term.lagging_change
130
+ term.lagging_change = 0
131
+ else
132
+ value -= term.last_interpol
133
+ end
134
+ term.last_interpol = interpol
135
+ value
136
+ end
137
+
138
+ private
139
+
140
+ # Set the variable arrays for the k values (k_steps)
141
+ def set_buffers
142
+ stages = butcher_table.stages
143
+ syms.rate_variables.each_value do |v|
144
+ v.k_steps = [*0..stages]
145
+ end
146
+ syms.terms_array.each { |s| s.k_steps = [*0..stages] }
147
+ end
148
+
149
+ # Calculates the k values of the Runge-Kutta methods.
150
+ def estimate_k_steps(step_size)
151
+ stages = (0..butcher_table.stages)
152
+ stages.each do |s|
153
+ calculate_stage(step_size, s, last: s == stages.max)
154
+ end
155
+ end
156
+
157
+ # Each individual k value calculation. NOTE: here the calculation k = h * F(t + ch, y + Σ(a_is)) instead of
158
+ # k = F(t + ch, y + Σ(a_is)) to resemble books on computer numerical methdos like Numerical Recipes.
159
+ # Effect: this updates the +k_steps+ values of +DifferentialSystemVariable+ and +DifferentialTerm+ and the
160
+ # buffer value for the next stage.
161
+ def calculate_stage(step_size, stage, last: false)
162
+ syms.ind_var.buffer_value = syms.ind_var.value + butcher_table.nodes[stage] * step_size
163
+ syms.rate_variables.each_value do |v|
164
+ v.k_steps[stage] = step_size * v.rate_equation.derivative_value(syms.syms_buffers_hash, stage)
165
+ v.terms.each_value { |tv| tv.k_steps[stage] *= step_size }
166
+ end
167
+ return if last
168
+
169
+ syms.rate_variables.each_value do |v|
170
+ v.buffer_value = v.value + (0..stage).map { |i|
171
+ butcher_table.matrix_coefficients[stage][i] * v.k_steps[i]
172
+ }.sum
173
+ end
174
+ end
175
+
176
+ # Performs the sum pondered with the Butcher table weights for the ks.
177
+ # Effect: updates +DifferentialSystemVariable.StepChange+ or +DifferentialSystemVariable.alternate_step_change+,
178
+ # depending on the +alternate+ parameter.
179
+ def combine_with_weighted(step_size, weights, alternate: false)
180
+ writer = alternate ? :alternate_step_change= : :step_change=
181
+ syms.rate_variables.each_value do |v|
182
+ v.send writer, v.k_steps[0..(weights.size - 1)].zip(weights)
183
+ .reduce(0) { |sum, e| sum + e[0] * e[1] }
184
+ end
185
+ # Calculation of separate term contributions is unnecessary for lesser order estimations
186
+ unless alternate
187
+ syms.terms_array.each do |t|
188
+ term_contribution = t.k_steps[0..weights.size].zip(weights).reduce(0) { |sum, e| sum + e[0] * e[1] }
189
+ t.register_step_change(term_contribution)
190
+ t.variable.term_changes[t.name] = term_contribution
191
+ end
192
+ end
193
+ syms.ind_var.send writer, step_size
194
+ end
195
+
196
+ # For adaptive methods, evaluates the error from the difference in estimation between the two weight sets
197
+ # of varying order considering the tolerances provided.
198
+ # @return [Hash{Symbol => Numeric}] A hash with the err index to judge whether to increase or reduce the step
199
+ # size and the factor by which the current step has to be multiplied.
200
+ def evaluate_error
201
+ vars = syms.vars_with_tols
202
+ # err calculation according to W. H. Press
203
+ relative_sq_sum = vars.values.reduce(0) do |acum, v|
204
+ acum + (v.delta / v.scale.to_f)**2
205
+ end
206
+ err = Math.sqrt(relative_sq_sum / vars.size)
207
+ # 0.9 is a recommended factor by Butcher
208
+ factor = 0.9 * (1 / err)**(1 / butcher_table.order.to_f)
209
+ { err: err, factor: factor }
210
+ end
211
+
212
+ # Part of the preparation which loads calculators for the interpolation functions.
213
+ def set_interpolation
214
+ @dense_calculators = []
215
+ butcher_table.interpolation_formulas.length.times { @dense_calculators.push Keisan::Calculator.new(cache: true) }
216
+ end
217
+
218
+ # Calculates the interpolated value of the variables or terms given the "slot", that is, the ordinal of the log
219
+ # for the given step.
220
+ # @param sym [DifferentialSystemVariable] The variable whose value is going to be calculated.
221
+ # @param point [Numeric] The independent variable value of the row to be recorded within the step.
222
+ def dense_interpolation(sym, point)
223
+ return tracker.current_logging_point if sym.instance_of?(DifferentialSystemVariable) && sym.independent?
224
+
225
+ data = sym.interpolator_hash.merge(butcher_table.interpolation_constants)
226
+ dense_calculators[0..-2].each_with_index do |c, j|
227
+ r_coef = c.evaluate(butcher_table.interpolation_formulas["r#{j + 1}"], data)
228
+ data.store "r#{j + 1}".to_sym, r_coef
229
+ end
230
+ data.store :theta, tracker.theta(point)
231
+ coef_keys = %i[r1 r2 r3 r4 r5 theta]
232
+ poly_coefs = data.slice(*coef_keys)
233
+ dense_calculators.last.evaluate(butcher_table.interpolation_formulas['final'], poly_coefs)
234
+ end
235
+ end
236
+ end
237
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'differential_system_symbol'
4
+
5
+ module Bibun
6
+ # Parameters lack buffer values and logs, but can take a reference interval to retrieve at random
7
+ class DifferentialSystemParameter < Bibun::DifferentialSystemSymbol
8
+ attr_accessor :reference_data
9
+
10
+ # In progress. A ReferenceData class is in mind to be able to pass reference ranges.
11
+ def sample
12
+ @value = @reference_data.random_value
13
+ end
14
+
15
+ # json serializer
16
+ def to_json(*_args)
17
+ { 'name' => name, 'symbol' => symbol, 'title' => title, 'unit' => unit, 'value' => value }.to_json
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bibun
4
+ # This class is not used directly, but implements behaviour common to DifferentialSystemVariable and
5
+ # DifferentialSystemParameter. Here 'symbol' refers to either a parameter or a variable.
6
+ class DifferentialSystemSymbol
7
+ # @return [String] The name of the symbol which is referenced in hashes and accessor methods.
8
+ attr_accessor :name
9
+ # @return [String] The string, usually short, which is used in string formulas for _Keisan::Calculator_
10
+ attr_accessor :symbol
11
+ # @return [String] A string to designate the variable for printing, so it can have spaces, dashes, etc.
12
+ attr_accessor :title
13
+ # @return [String] A short string to document the units of the symbol
14
+ attr_accessor :unit
15
+ # @return [Numeric] The numeric value which the symbol holds at one given moment.
16
+ attr_accessor :value
17
+
18
+ # Regular initialization.
19
+ def initialize(name:, symbol:, title: nil, value: nil, unit: nil)
20
+ @name = name
21
+ @symbol = symbol
22
+ @title = title
23
+ @value = value
24
+ @unit = unit
25
+ end
26
+
27
+ # Method to express the current value of the symbol with units (e.g. "30 m")
28
+ # @param significant_figures[Integer] The decimal places to round the value of the symbol.
29
+ # @return [String] The value with units.
30
+ def with_units(significant_figures = nil)
31
+ quantity = significant_figures.nil? ? value : value.round(significant_figures).to_s
32
+ "#{quantity} #{@unit}"
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bibun
4
+ # Class to implement variables (in this library, variables change their value as the independent variable change,
5
+ # parameters keep their value fixed across the simulation)
6
+ class DifferentialSystemVariable < Bibun::DifferentialSystemSymbol
7
+
8
+ # Reference to the +SubDifferentialEquation+ object that contains the function for its derivative.
9
+ attr_accessor :rate_equation
10
+ # Adds a custom type which can be useful for grouping variables and subclassing.
11
+ attr_accessor :type
12
+ # Reference to the quantity change that is caused by a integration step and which is eventually added to the value.
13
+ attr_accessor :step_change
14
+ # For adaptive embedded methods, it stores the quantity change calculated with the alternative set of weights of
15
+ # a different order in order to calculate the error of the intergration step.
16
+ attr_accessor :alternate_step_change
17
+ # This is hash with the quantity changes caused by each of the rate additive terms, when those are defined.
18
+ # Their sum equals the step change.
19
+ attr_accessor :term_changes
20
+ # Absolute tolerance to consider for adpative methods.
21
+ attr_accessor :atol
22
+ # Relative tolerance to consider for adaptive methods.
23
+ attr_accessor :rtol
24
+ # Array which holds the k quantities (k1, k2, k3, etc in Runge Kutta methods) corresponding to each stage of the
25
+ # Runge Kutta method. Since arrays start in zero, add 1 to be equal to the book formulas.
26
+ attr_accessor :k_steps
27
+ # Value for the variable to be taken for the evaluation of the next k and which is calcualted using the matrix
28
+ # coefficients a11, a21, a22, a31, a32, a33... of the Butcher Table and which does not require further storing.
29
+ # Also used as temporary cache for expression calculations when logging.
30
+ attr_accessor :buffer_value
31
+ # Boolean to distinguish the independent variable from the dependent variables.
32
+ attr_reader :independent
33
+ alias independent? independent
34
+
35
+ # Regular initialization.
36
+ def initialize(name:, symbol:, title: nil, value: nil, unit: nil, type: nil, rate_function: nil,
37
+ atol: nil, rtol: nil, independent: false)
38
+ super(name: name, symbol: symbol, title: title, value: value, unit: unit)
39
+ @type = type
40
+ @term_changes = {}
41
+ @rate_equation = SubDifferentialEquation.new(self) unless type == :independent_variable
42
+ @atol = atol || 0
43
+ @rtol = rtol || 0
44
+ @independent = independent
45
+ rate_equation.rate_function = rate_function unless rate_function.nil?
46
+ end
47
+
48
+ # Shortcut attribute writer for the rate function of the _SubDifferentialEquation_ object
49
+ # @param [String] formula The mathematical expression of the formula in native Ruby.
50
+ # @example
51
+ # variables['temperature'].rate_function = '3 * t^(1.5)'
52
+ def rate_function=(formula)
53
+ rate_equation.rate_function = formula if rate_equation.terms.empty?
54
+ end
55
+
56
+ # Shortcut method to adds a rate term to the _SubDifferentialEquation_ object.
57
+ # see {SubDifferentialEquation#add_rate_term}
58
+ def add_rate_term(name, formula)
59
+ rate_equation.add_rate_term name, formula
60
+ end
61
+
62
+ # Routine to convert to json.
63
+ def to_json(*_args)
64
+ basic_hash = { 'name' => name, 'symbol' => symbol, 'title' => title, 'unit' => unit }
65
+ rates_hash = if rate_equation.terms.empty?
66
+ { 'rate_function' => rate_equation.rate_function }
67
+ else
68
+ { 'terms' => rate_equation.terms.values }
69
+ end
70
+ basic_hash.merge(rates_hash).to_json
71
+ end
72
+
73
+ # Formula to evaluate error for adaptive methods. From Numerical Recipes by W. H. Press.
74
+ # Notice how both tolerances influence the result so that is always higher than either one of them.
75
+ # @return [Float] the combined value of the scale.
76
+ def scale = atol + rtol * [value + step_change, value + alternate_step_change].max
77
+
78
+ # Difference between the estimations of embedded methods (the actual value cancels out).
79
+ # @return [Float] the difference between both estimations.
80
+ def delta = step_change - alternate_step_change
81
+
82
+ # Quick method to check if the variable has additive rate terms defined.
83
+ # @return [Boolean]
84
+ def terms? = !rate_equation.terms.empty?
85
+
86
+ # Returns the hash of rate additive terms. Shortcut to access from the variable itself.
87
+ # @return [Hash{String => DifferentialTerm}] The hash with the additive terms for the variable.
88
+ def terms = rate_equation.terms
89
+
90
+ # Hash with the values necessary for polynomial interpolation for dense outputs. This is fed as the second
91
+ # argument for Keisan::Calculator#evaluate.
92
+ # @return [Hash{Symbol => Float}] the hash with the key value pairs.
93
+ def interpolator_hash
94
+ vals = { y0: value, y1: value + step_change }
95
+ k_hash = k_steps.map.with_index { |k, i| ["k#{i + 1}".to_sym, k] }.to_h
96
+ vals.merge(k_hash)
97
+ end
98
+
99
+ # Class that handle the differential equation which governs the variable's rate of change. Shortcut methods on the
100
+ # variable make its access non essential, but are provided anyway.
101
+ class SubDifferentialEquation
102
+ attr_accessor :name, :rate_function, :terms, :current_value, :rate_variable
103
+
104
+ def initialize(parent_variable, rate_formula = nil, with_terms: false)
105
+ @rate_variable = parent_variable
106
+ @name = parent_variable.name
107
+ @terms = {}
108
+ @rate_function = rate_formula
109
+ @calculator = Keisan::Calculator.new(cache: true)
110
+ end
111
+
112
+ # Method to add a rate term. Automatically turns the existing rate function into another term to avoid mixing
113
+ # the two.
114
+ # @param [String] name The name of the rate term to be used for referencing
115
+ # @param [String] formula The mathematical expression for the term in native Ruby.
116
+ def add_rate_term(name, formula)
117
+ unless rate_function.nil?
118
+ @terms.store 'base_rate', DifferentialTerm.new('base_rate', rate_function, self)
119
+ @rate_function = nil
120
+ end
121
+ @terms&.store name, DifferentialTerm.new(name, formula, self)
122
+ end
123
+
124
+ # This part evaluates the derivative for the values provided.
125
+ # @param [Hash{String => Numeric}] values_hash The hash with the values of the variables and parameters
126
+ # @param [Integer] stage The stage of the Runge-Kutta method. Used to store the value in the proper array slot.
127
+ def derivative_value(values_hash, stage)
128
+ if @terms.empty?
129
+ @calculator.evaluate(@rate_function, values_hash)
130
+ else
131
+ @terms.values.reduce(0) do |sum, term|
132
+ contr = term.contribution(values_hash)
133
+ term.k_steps[stage] = contr
134
+ sum + contr
135
+ end
136
+ end
137
+ end
138
+
139
+ # It is common to have several distinct additive contributions to the derivatives of variables.
140
+ # This class enables to create different contributions (rate additive terms) that can be summed.
141
+ class DifferentialTerm
142
+ attr_accessor :name, :rate_function, :k_steps, :variable, :complementary, :last_mark, :last_interpol
143
+ # This is the change in the variable through the integration steps that is caused by the particular
144
+ # rate additive term. The sum of all rate additive terms is the variable step_change.
145
+ # @return [Float]
146
+ attr_accessor :step_change
147
+ # This attribute is necessary when a logging_interval is specified in order to track the total change
148
+ # between two logging points across steps.
149
+ # @return [Float]
150
+ attr_accessor :cumulative_change
151
+ # For dense output, tracks change not logged yet.
152
+ attr_accessor :lagging_change
153
+
154
+ def initialize(name, formula, equation)
155
+ @name = name
156
+ @rate_function = formula
157
+ @calculator = Keisan::Calculator.new(cache: true)
158
+ @variable = equation.rate_variable
159
+ @equation = equation
160
+ @cumulative_change, @lagging_change = 0, 0
161
+ end
162
+
163
+ # This part evaluates the contribution for the values provided. See {SubDifferentialEquation#derivative_value}.
164
+ # Called by the parent equation on each rate additive term to sum the contributions later.
165
+ def contribution(values_hash)
166
+ @calculator.evaluate(@rate_function, values_hash)
167
+ end
168
+
169
+ # Serializer to json for the overall DES json object.
170
+ def to_json(*_args)
171
+ { 'name' => name, 'rate_function' => rate_function }.to_json
172
+ end
173
+
174
+ # Provides value for dense output polynomial interpolation. For additive terms, only the step contribution is
175
+ # computed so starting value is zero by default.
176
+ # @return [Hash{Symbol => Numeric}] The hash with key value pairs.
177
+ def interpolator_hash
178
+ vals = { y0: 0, y1: step_change }
179
+ k_hash = k_steps.map.with_index { |k, i| ["k#{i + 1}".to_sym, k] }.to_h
180
+ vals.merge(k_hash)
181
+ end
182
+
183
+ # When writing the step change, adds to the cumulative change in order to keep total across the steps for
184
+ # logging.
185
+ def register_step_change(amount)
186
+ @step_change = amount
187
+ @cumulative_change += amount
188
+ end
189
+
190
+ # Gives the total cumulative change and resets it to zero for future logging
191
+ def report_and_reset
192
+ c = @cumulative_change
193
+ @cumulative_change = 0
194
+ c
195
+ end
196
+
197
+ # Gives the concatenation variable_name:term_name from the object itself.
198
+ def id = "#{variable.name}:#{name}"
199
+ end
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bibun
4
+ class DifferentialEquationSystem
5
+ class DifferentialEquationLogger
6
+ # Class to handle each column or quantity that is set to be logged (variables, terms, custom expressions...).
7
+ # Generic class. Only specific subclasses are meant to be instanced.
8
+ # Contains attributes for all subclasses to implement polymorphism
9
+ class LogEntry
10
+ # The array with the values at each row.
11
+ attr_accessor :values, :stepper, :logger, :tracker, :buffer_hash, :decimals, :type
12
+
13
+ # Standard initializer. Subclasses receive the object which are meant to track and log.
14
+ def initialize(logger, decimals)
15
+ @values = []
16
+ @stepper = logger.system.stepper
17
+ @logger = logger
18
+ @tracker = logger.tracker
19
+ @decimals = decimals
20
+ end
21
+
22
+ # Method to add one row value to the values array.
23
+ def register(marks = nil, index = nil)
24
+ values << loggable(marks, index).round(decimals)
25
+ end
26
+
27
+ # Shortcut method to access the values without having to type values each time.
28
+ def [](index)
29
+ values[index]
30
+ end
31
+
32
+ # Stub method for inheriting.
33
+ def loggable(marks, index); end
34
+
35
+ # Basic hash to be used for serializing to JSON.
36
+ def basic_hash(*_args)
37
+ { 'type' => @type, 'decimals' => @decimals }
38
+ end
39
+ end
40
+
41
+ # Subclass for logging values of variables
42
+ class VariableLogEntry < LogEntry
43
+ attr_reader :variable
44
+
45
+ def initialize(logger, decimals, var_name)
46
+ super(logger, decimals)
47
+ @variable = var_name
48
+ @type = 'variable'
49
+ end
50
+
51
+ # Asks the stepper for the value to log given the marks array and the index.
52
+ def loggable(marks,index) = stepper.variable_log_value(var_obj, marks, index)
53
+
54
+ # Necessary for logging the starting values
55
+ def register_value
56
+ values << logger.syms.variables[variable].value
57
+ end
58
+
59
+ # Quick way to get the variable object rather than its name.
60
+ def var_obj = logger.syms.variables[variable]
61
+
62
+ # Serializer for the outer class.
63
+ def to_json(*_args)
64
+ basic_hash.merge({ 'variable' => variable }).to_json
65
+ end
66
+ end
67
+
68
+ # Subclass to log rate additive terms
69
+ class TermLogEntry < LogEntry
70
+ attr_reader :variable, :term
71
+
72
+ def initialize(logger, decimals, var_name, term_name)
73
+ super(logger, decimals)
74
+ @variable = var_name
75
+ @term = term_name
76
+ @type = 'term'
77
+ end
78
+
79
+ # Asks the stepper for the value to log providing the marks array and the index.
80
+ def loggable(marks, index)
81
+ stepper.term_log_value(term_obj, marks, index)
82
+ end
83
+
84
+ # Quick way to get the rate additive term object.
85
+ def term_obj = logger.syms.variables[variable].terms[term]
86
+
87
+ # Serializer to json
88
+ def to_json(*_args)
89
+ basic_hash.merge({ 'variable' => variable, 'term' => term }).to_json
90
+ end
91
+ end
92
+
93
+ # Subclass for logging the current step
94
+ class StepLogEntry < LogEntry
95
+ def initialize(logger, decimals)
96
+ super(logger, decimals)
97
+ @type = 'step'
98
+ end
99
+
100
+ # Gives the current logging step for logging.
101
+ def loggable(*args) = tracker.current_logging_step
102
+ # Serializer to JSON
103
+ def to_json(*_args) = basic_hash.to_json
104
+ end
105
+
106
+ # Subclass for logging custom math expressions.
107
+ class ExpressionLogEntry < LogEntry
108
+ # @return [String] The mathematical expression for Keisan in plain Ruby syntax.
109
+ attr_accessor :expression
110
+ # Unlike other entries, expressions require to know their name since it is custom made.
111
+ attr_accessor :name
112
+
113
+ def initialize(logger, decimals, name, expression)
114
+ super(logger, decimals)
115
+ @expression = expression
116
+ @calculator = Keisan::Calculator.new(cache: true)
117
+ @type = 'expression'
118
+ @name = name
119
+ end
120
+
121
+ # Gives the calculated value for the expression for logging.
122
+ def loggable(*args) = @calculator.evaluate(expression, buffer_hash)
123
+
124
+ # Serializer to JSON.
125
+ def to_json(*_args)
126
+ basic_hash.merge('name' => name, 'formula' => expression).to_json
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end