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.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +3 -0
- data/CHANGELOG.md +7 -0
- data/LICENSE.txt +26 -0
- data/README.md +909 -0
- data/lib/bibun/differential_butcher_table.rb +72 -0
- data/lib/bibun/differential_equation_logger.rb +157 -0
- data/lib/bibun/differential_equation_system.rb +367 -0
- data/lib/bibun/differential_stepper.rb +237 -0
- data/lib/bibun/differential_system_parameter.rb +21 -0
- data/lib/bibun/differential_system_symbol.rb +35 -0
- data/lib/bibun/differential_system_variable.rb +202 -0
- data/lib/bibun/log_entry.rb +131 -0
- data/lib/bibun/step_tracker.rb +129 -0
- data/lib/bibun/symbol_group.rb +100 -0
- data/lib/bibun/version.rb +6 -0
- data/lib/bibun.rb +22 -0
- data/lib/data/butcher_tables.toml +44 -0
- data.tar.gz.sig +0 -0
- metadata +255 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bibun
|
|
4
|
+
# Class that stores the data relative to the Butcher table used to perform the integration.
|
|
5
|
+
class DifferentialButcherTable
|
|
6
|
+
# The Butcher table nodes or c values. At each stage ti = tn + ch.
|
|
7
|
+
# @return [Array<Numeric>]
|
|
8
|
+
attr_accessor :nodes
|
|
9
|
+
# The Butcher node coefficients or a values. At each stage yi = y1ai1 + y2ai2 + y3ai3...y[i-1]ai[i-1]
|
|
10
|
+
# @return [Array<Numeric>]
|
|
11
|
+
attr_accessor :matrix_coefficients
|
|
12
|
+
# The weights by which each k_step value is multiplied before summing them into the final variable change.
|
|
13
|
+
# They are the Butcher table b values such as k = k1b1 + k2b2...
|
|
14
|
+
# @return [[Array<Numeric>]]
|
|
15
|
+
attr_accessor :weights
|
|
16
|
+
# For embedded methods they are another set of weights which give an estimate of the change for the variable of
|
|
17
|
+
# lesser order and against which the reference calculation is compared for estimating the error..
|
|
18
|
+
# They are the Butcher table b* values such as k = k1b*1 + k2b*2...
|
|
19
|
+
# @return [[Array<Numeric>]]
|
|
20
|
+
attr_accessor :alternate_weights
|
|
21
|
+
# @return [Boolean] Indicates whether the butcher table method is adaptive or not.
|
|
22
|
+
attr_reader :adaptive
|
|
23
|
+
# @return [Integer] The order of the method.
|
|
24
|
+
attr_reader :order
|
|
25
|
+
# @return [Hash{String => Numeric}] Constants for polynomomial interpolation.
|
|
26
|
+
attr_reader :interpolation_constants
|
|
27
|
+
# @return [Hash{String => String}] Formulas to calculate coefficients for polynomomial interpolation.
|
|
28
|
+
attr_reader :interpolation_formulas
|
|
29
|
+
alias adaptive? adaptive
|
|
30
|
+
|
|
31
|
+
# List of methods which can be preloaded. rk4 is the default one for non adaptive and dopr45 for adaptive.
|
|
32
|
+
DIFFERENTIATION_METHODS = %i[rk4 rk3 midpoint dopr45].freeze
|
|
33
|
+
|
|
34
|
+
# Initialization.
|
|
35
|
+
def initialize
|
|
36
|
+
@weights, @nodes, @matrix_coefficients = [], [], []
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Because a Butcher Table might be provided by the user, evaluates if the arrays are empty to choose whether to
|
|
40
|
+
# load a default or not.
|
|
41
|
+
# @return [Boolean]
|
|
42
|
+
def empty?
|
|
43
|
+
weights.union(nodes, matrix_coefficients).empty?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# @return [Integer] The number of stages for array indexing.
|
|
47
|
+
def stages
|
|
48
|
+
weights.size - 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Loads a preset Butcher Table defined in the inner TOML file.
|
|
52
|
+
def load_preset(method:)
|
|
53
|
+
data_path = File.expand_path('../data/butcher_tables.toml', __dir__)
|
|
54
|
+
rationalize = ->(s) { s.is_a?(String) ? s.to_r : s }
|
|
55
|
+
rationalize_array = ->(ar) { ar.map! { |val| rationalize.call(val) } }
|
|
56
|
+
toml = Tomlib.load File.read(data_path)
|
|
57
|
+
toml = toml[method.to_s]
|
|
58
|
+
@order = toml['order']
|
|
59
|
+
@nodes = rationalize_array.call(toml['nodes'])
|
|
60
|
+
@weights = rationalize_array.call(toml['weights'])
|
|
61
|
+
@matrix_coefficients = toml['matrix_coefficients']
|
|
62
|
+
matrix_coefficients.each { |ar| rationalize_array.call(ar) }
|
|
63
|
+
@adaptive = toml['adaptive']
|
|
64
|
+
if adaptive?
|
|
65
|
+
@alternate_weights = rationalize_array.call(toml['alternate_weights'])
|
|
66
|
+
@interpolation_constants = toml['interpolation']['constants'].transform_values { |v| rationalize.call(v) }
|
|
67
|
+
@interpolation_formulas = toml['interpolation']['coefficients']
|
|
68
|
+
interpolation_formulas.store 'final', toml['interpolation']['formula']['final']
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'csv'
|
|
4
|
+
|
|
5
|
+
module Bibun
|
|
6
|
+
class DifferentialEquationSystem
|
|
7
|
+
# Class to handle the logging of values calculated through the simulation of differential equations.
|
|
8
|
+
# This class is not meant to be used or instantiated independently, the DES instance already creates one when
|
|
9
|
+
# initialized. Should be always accesed through the DifferentialEquationSystem#logger getter.
|
|
10
|
+
class DifferentialEquationLogger
|
|
11
|
+
# @return [Hash{String => LogEntry}] Each of the entries or columns to register in the log.
|
|
12
|
+
attr_accessor :entries
|
|
13
|
+
# @return [SymbolGroup] Accessor to the SymbolGroup of the DES
|
|
14
|
+
attr_reader :syms
|
|
15
|
+
attr_accessor :buffer, :has_expressions, :system, :tracker
|
|
16
|
+
attr_reader :logging_interval
|
|
17
|
+
|
|
18
|
+
def initialize(system, tracker, syms)
|
|
19
|
+
@entries = {}
|
|
20
|
+
@logs = {}
|
|
21
|
+
@order = []
|
|
22
|
+
@buffer = {}
|
|
23
|
+
@has_expressions = false
|
|
24
|
+
@system = system
|
|
25
|
+
@syms = syms
|
|
26
|
+
@tracker = tracker
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Direct access in a hash style to the entries' arrays of values.
|
|
30
|
+
# @return [Hash{String => Array<Numeric>}]
|
|
31
|
+
def logs = entries.transform_values(&:values)
|
|
32
|
+
|
|
33
|
+
# Adds an arbitrary number of variables to the logger.
|
|
34
|
+
def add_variables(vars_decimals)
|
|
35
|
+
vars_decimals.each { |k, v| entries.store k, VariableLogEntry.new(self, v, k) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Method to add an specific additive term pertaining an specific variable.
|
|
39
|
+
def add_rate_term(variable_name, term_name, decimals)
|
|
40
|
+
entries.store "#{variable_name}:#{term_name}", TermLogEntry.new(self, decimals, variable_name, term_name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The equivalent of a database ROW_NUMBER, for those cases where time is not an integer.
|
|
44
|
+
def add_step_state
|
|
45
|
+
entries.store 'step', StepLogEntry.new(self, 0)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Way to add custom expressions whose calculation is desired to be tracked and logged.
|
|
49
|
+
def add_expression(name, formula, decimals)
|
|
50
|
+
entries.store name, ExpressionLogEntry.new(self, decimals, name, formula)
|
|
51
|
+
@has_expressions = true
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Adds a single variable to the logger along its additive terms. All terms are added by default, but this
|
|
55
|
+
# behaviour can be overriden by passing a list including only the target terms.
|
|
56
|
+
def add_variable_with_terms(variable, decimals, *only)
|
|
57
|
+
add_variables({ variable => decimals })
|
|
58
|
+
stream_array = only.empty? ? syms.variables[variable].terms.keys : only
|
|
59
|
+
stream_array.each { |s| add_rate_term(variable, s, decimals) }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# To avoid having to write a decimal specification for each variable, this methods allows to add several variables
|
|
63
|
+
# simultaneously while only passing a single decimal specification for all of them.
|
|
64
|
+
def add_same_decimals_to_variables(decimals, *variable_group)
|
|
65
|
+
variable_group.each { |v| add_variables({ v => decimals }) }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Quickest way to set a logger. All is needed is to provide the decimals once. The with_streams: keyword argument
|
|
69
|
+
# allows to have all terms from all variables logged.
|
|
70
|
+
def add_all(decimals, with_terms: false)
|
|
71
|
+
if with_terms
|
|
72
|
+
add_variables({ syms.ind_var.name => decimals })
|
|
73
|
+
syms.rate_variables.each_key do |k|
|
|
74
|
+
add_variable_with_terms(k, decimals)
|
|
75
|
+
end
|
|
76
|
+
else
|
|
77
|
+
add_variables(syms.variables.transform_values { |_| decimals })
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Quick method to check if there is any rate additive term to log.
|
|
82
|
+
def log_terms? = entries.values.any? { |le| le.instance_of?(TermLogEntry) }
|
|
83
|
+
|
|
84
|
+
# Quick way to access the terms.
|
|
85
|
+
# @return [Array<TermLogEntry>] The array with the log entries.
|
|
86
|
+
def term_entries = entries.values.select { |et| et.instance_of?(TermLogEntry) }
|
|
87
|
+
|
|
88
|
+
# Preparation routine. Logs the initial values of the variables.
|
|
89
|
+
def prepare
|
|
90
|
+
start_logs
|
|
91
|
+
log_start
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Caches the parameters for expression calculations
|
|
95
|
+
def start_logs
|
|
96
|
+
@parameters = syms.pars_values_hash if @has_expressions
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Registering the log interval. Method is placed here because it is intuitive since it pertains the logging.
|
|
100
|
+
def logging_interval=(independent_variable_interval)
|
|
101
|
+
tracker.logging_interval = independent_variable_interval
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Adds the first row to the log comprised of the starting values and expressions calculated from them.
|
|
105
|
+
def log_start
|
|
106
|
+
entries.values.reject {|v| v.instance_of?(TermLogEntry) }.each do |ls|
|
|
107
|
+
ls.buffer_hash = syms.syms_values_hash
|
|
108
|
+
if ls.instance_of?(VariableLogEntry)
|
|
109
|
+
ls.register_value
|
|
110
|
+
else
|
|
111
|
+
ls.register # Step and Expressions
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Core method that adds a row to the logs and gets called once per each step walked.
|
|
117
|
+
# The marks are the logging points by the walking step which are 'marked' to be recorded.
|
|
118
|
+
def log_forward
|
|
119
|
+
marks = tracker.marks_to_advance
|
|
120
|
+
marks.each_index do |i|
|
|
121
|
+
tracker.log_forward marks[i]
|
|
122
|
+
prepare_expression_buffers(marks, i) if @has_expressions
|
|
123
|
+
entries.each_value { |e| e.register(marks, i) }
|
|
124
|
+
end
|
|
125
|
+
if tracker.dense_output?
|
|
126
|
+
term_entries.each do |et|
|
|
127
|
+
already_covered = marks.empty? ? 0 : et.term_obj.last_interpol
|
|
128
|
+
et.term_obj.lagging_change += et.term_obj.step_change - already_covered
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
tracker.last_mark = tracker.current_logging_point
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Right now I have no way to get the variables from a Keisan AST so if there is an expression then I calculate
|
|
135
|
+
# the values of all variables and put them in the buffer for the correct expression evaluation.
|
|
136
|
+
# TODO: Find a way to extract the variables of the AST to limit only to the ones necessary.
|
|
137
|
+
def prepare_expression_buffers(marks, index)
|
|
138
|
+
syms.rate_variables.each_value do |v|
|
|
139
|
+
v.buffer_value = system.stepper.variable_log_value(v, marks, index)
|
|
140
|
+
end
|
|
141
|
+
hash = syms.syms_buffers_hash
|
|
142
|
+
entries.values.select { |le| le.instance_of?(ExpressionLogEntry) }
|
|
143
|
+
.each { |le| le.buffer_hash = hash }
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Once simulation is over, records all loggable elements to a csv file in the order they were added.
|
|
147
|
+
def save_to_file(file_name)
|
|
148
|
+
CSV.open(file_name, 'w', headers: true) do |csv|
|
|
149
|
+
csv << entries.keys
|
|
150
|
+
(0..tracker.current_logging_step).each do |row|
|
|
151
|
+
csv << entries.map { |_, v| v[row] }
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Bibun
|
|
4
|
+
# TODO
|
|
5
|
+
# Add more functionality to put the time steps in actual time units
|
|
6
|
+
# Add capability to detect and stop the simulation when a variable becomes stationary.
|
|
7
|
+
# Use Ractors in the Stepper to optimize
|
|
8
|
+
|
|
9
|
+
# Class to integrate systems of ordinary differential equations by numerical methods.
|
|
10
|
+
# Used mostly to integrate runs using Runge-Kutta or Dormand-Prince methods.
|
|
11
|
+
# From here on DES => Differential Equation System.
|
|
12
|
+
# @author Javier Oswaldo Hernández Batis
|
|
13
|
+
class DifferentialEquationSystem
|
|
14
|
+
# Holds the SymbolGroup object which contains the variables and parameters of the DES.
|
|
15
|
+
attr_accessor :syms
|
|
16
|
+
# Holds the stepper object which executes the integration steps.
|
|
17
|
+
attr_accessor :stepper
|
|
18
|
+
# Holds the logger objects which is in charge of logging the variable and other values taken at each step.
|
|
19
|
+
attr_accessor :logger
|
|
20
|
+
# Holds the step tracker object which keeps track of the independent variable and step positions through the
|
|
21
|
+
# integration
|
|
22
|
+
attr_accessor :tracker
|
|
23
|
+
# Global boolean which can display useful data for debugging.
|
|
24
|
+
attr_accessor :debug_mode
|
|
25
|
+
alias debug_mode? debug_mode
|
|
26
|
+
|
|
27
|
+
# Adds the four inner objects for reference and manipulation and time as the default independent variable.
|
|
28
|
+
def initialize
|
|
29
|
+
@tracker = StepTracker.new
|
|
30
|
+
@syms = SymbolGroup.new(self)
|
|
31
|
+
@logger = DifferentialEquationLogger.new(self, @tracker, @syms)
|
|
32
|
+
@stepper = DifferentialStepper.new(self, @tracker, @syms)
|
|
33
|
+
|
|
34
|
+
@debug_mode = false
|
|
35
|
+
add_independent 'time', 't', 'Time'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Basic method to add a dependent variable to the DES.
|
|
39
|
+
# @param name [String] Name to use for accessing the variable in the variables hash and through the generated
|
|
40
|
+
# accessor method. Use caveats for method naming (no dashes or spaces, etc). If only one ordered argument is
|
|
41
|
+
# passed, the symbol will get the same value as the variable name and no accessor method will be left.
|
|
42
|
+
# @param symbol [String] String used in formulas for Keisan. Recommended to be short as in 't' for time.
|
|
43
|
+
# @param title [String] Longer name which could be used for presentation and printing which unlike name has no
|
|
44
|
+
# restrictions.
|
|
45
|
+
# @param unit [String] The units of the variable, for printing and reporting.
|
|
46
|
+
# @param type [Symbol] A custom type that can be added for variable grouping; for example :concentration for
|
|
47
|
+
# substance concentrations in reactor systems.
|
|
48
|
+
# @param rate_function [String] The string that contains the mathematical formula for the derivative of the variable.
|
|
49
|
+
# Will be used by Keisan. Use regular Ruby syntax. For example if the differential equation is
|
|
50
|
+
# dq / dt = 2 t**2 - 3 * q, introduce '2 t**2 - 3 * q'
|
|
51
|
+
# @param atol [Float] The absolute tolerance for error to be used in adaptive method. At least one dependant variable
|
|
52
|
+
# must have a nonzero value for either absolute or relative tolerance.
|
|
53
|
+
# @param rtol [Float] The relative tolerance for error to be used in adaptive method.
|
|
54
|
+
# @param accessor [Boolean] Controls if an accessor method is created for the variable using its name argument.
|
|
55
|
+
# @return [DifferentialSystemVariable] The variable created.
|
|
56
|
+
# @example Add a Volume variable to represent a basin volume
|
|
57
|
+
# add_variable 'volume', 'V', 'Basin Volume', 'm3', rate_function: 'F - eV'
|
|
58
|
+
def add_variable(name, symbol = nil, title = nil, unit = nil, type: nil, rate_function: nil, atol: 0, rtol: 0,
|
|
59
|
+
accessor: true)
|
|
60
|
+
|
|
61
|
+
symbol_provided = !symbol.nil?
|
|
62
|
+
symbol = name unless symbol_provided
|
|
63
|
+
var = DifferentialSystemVariable.new(name: name, symbol: symbol, title: title,unit: unit, type:, rate_function:,
|
|
64
|
+
atol:, rtol:)
|
|
65
|
+
variables.store name.downcase, var
|
|
66
|
+
return unless accessor && symbol_provided
|
|
67
|
+
|
|
68
|
+
instance_variable_set "@#{name.downcase}", var
|
|
69
|
+
define_singleton_method name.downcase do
|
|
70
|
+
instance_variable_get "@#{name.downcase}"
|
|
71
|
+
end
|
|
72
|
+
var
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Method to edit the independent variable, which is created automatically.
|
|
76
|
+
# @param var_name [String] Name to use for accessing the variable in the variables hash and through the generated
|
|
77
|
+
# accessor method. Use caveats for method naming (no dashes or spaces, etc). If only one ordered argument is
|
|
78
|
+
# passed, the symbol will get the same value as the variable name and no accessor method will be left.
|
|
79
|
+
# @param symbol [String] String used in formulas for Keisan. Recommended to be short as in 't' for time.
|
|
80
|
+
# @param title [String] Longer name which could be used for presentation and printing which unlike name has no
|
|
81
|
+
# restrictions.
|
|
82
|
+
# @param unit [String] The units of the variable, for printing and reporting.
|
|
83
|
+
# @param accessor [Boolean] If false, removes the accesor method.
|
|
84
|
+
# @example Changing to the usual x for x and y differential equations.
|
|
85
|
+
# edit_independent_variable 'x'
|
|
86
|
+
def edit_independent_variable(var_name, symbol = nil, title = nil, unit = nil, accessor: true)
|
|
87
|
+
symbol_provided = !symbol.nil?
|
|
88
|
+
ind = syms.ind_var
|
|
89
|
+
old_key = variables.key(ind)
|
|
90
|
+
symbol = var_name unless symbol_provided
|
|
91
|
+
ind.name, ind.symbol, ind.title, ind.unit = var_name, symbol, title, unit
|
|
92
|
+
variables[var_name] = variables.delete old_key
|
|
93
|
+
singleton_class.undef_method(old_key)
|
|
94
|
+
return unless accessor && symbol_provided
|
|
95
|
+
|
|
96
|
+
instance_variable_set "@#{var_name.downcase}", ind
|
|
97
|
+
define_singleton_method var_name.downcase do
|
|
98
|
+
instance_variable_get "@#{var_name.downcase}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Basic method to add a dependent variable to the DES.
|
|
103
|
+
# @param name [String] Name to use for accessing the variable in the variables hash and through the generated
|
|
104
|
+
# accessor method. Use caveats for method naming (no dashes or spaces, etc). If only one ordered argument is
|
|
105
|
+
# passed, the symbol will get the same value as the variable name and no accessor method will be left.
|
|
106
|
+
# @param symbol [String] String used in formulas for Keisan. Recommended to be short as in 't' for time.
|
|
107
|
+
# @param title [String] Longer name which could be used for presentation and printing which unlike name has no
|
|
108
|
+
# restrictions.
|
|
109
|
+
# @param unit [String] The units of the variable, for printing and reporting.
|
|
110
|
+
# @param value [Numeric] Allows to set the value of the parameter.
|
|
111
|
+
# @param accessor [Boolean] Controls if an accessor method is created for the variable using its name argument.
|
|
112
|
+
# @return [DifferentialSystemParameter] The parameter created.
|
|
113
|
+
# @example Add a rate coefficient ára,eter-
|
|
114
|
+
# add_parameter 'rate_coefficient', 'r', 'Rate Coefficient', 'h-1', value: 0.52
|
|
115
|
+
def add_parameter(name, symbol = nil, title = nil, unit = nil, value: nil, accessor: true)
|
|
116
|
+
symbol_provided = !symbol.nil?
|
|
117
|
+
symbol = name unless symbol_provided
|
|
118
|
+
par = DifferentialSystemParameter.new name: name, symbol: symbol, title: title,
|
|
119
|
+
unit: unit, value: value
|
|
120
|
+
parameters.store name.downcase, par
|
|
121
|
+
return unless accessor && symbol_provided
|
|
122
|
+
|
|
123
|
+
instance_variable_set("@#{name.downcase}", par)
|
|
124
|
+
define_singleton_method name.downcase do
|
|
125
|
+
instance_variable_get "@#{name.downcase}"
|
|
126
|
+
end
|
|
127
|
+
par
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Shortcut method which adds multiple variable in quick form (name identical to symbol, no accessor) along their
|
|
131
|
+
# rate variable.
|
|
132
|
+
# @param vars_hash [Hash{String => String}] The hash with the variable names as keys and rate function as values.
|
|
133
|
+
# @example
|
|
134
|
+
# add_variables({'y' => '3 * t - 2', 'z' => '2 * y - t'})
|
|
135
|
+
def add_variables(vars_hash)
|
|
136
|
+
vars_hash.each { |k, v| add_variable k, rate_function: v }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Similar to add_variables, add multiple parameters in quick style (name identical to symbol, no accessor) along
|
|
140
|
+
# their values.
|
|
141
|
+
# @param pars_hash [Hash{String => Numeric}] The hash with the parameters names and values.
|
|
142
|
+
# @example
|
|
143
|
+
# add_parameters({ a => 3.5, b => 3/5r })
|
|
144
|
+
def add_parameters(pars_hash)
|
|
145
|
+
pars_hash.each { |k, v| add_parameter k, value: v }
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Shortcut writer method to edit the step_size attribute of the tracker object.
|
|
149
|
+
# @param step_size [Numeric] the width of the step_size.
|
|
150
|
+
def step_size=(step_size)
|
|
151
|
+
tracker.step_size = step_size
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Shortcut writer method to edit the duration attribute of the step_tracker object.
|
|
155
|
+
# @param duration [Numeric] the total duration or timespan of the integration.
|
|
156
|
+
def duration=(duration)
|
|
157
|
+
tracker.duration = duration
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Shortcut method to add uniform tolerances to all dependent variable. Not recommended if variables are of
|
|
161
|
+
# different units or priority. See {#add_variable}
|
|
162
|
+
def set_global_tolerance(atol = 0, rtol = 0)
|
|
163
|
+
rate_variables.each { |v| v.atol = atol; v.rtol = rtol }
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# Way to assign values for both parameters and variables for the start of the run. Shortcut to the method of the
|
|
167
|
+
# syms oject.
|
|
168
|
+
# @param values_hash [Hash{String => Numeric}] Hash with the variable and parameter names and their intended values.
|
|
169
|
+
# @example
|
|
170
|
+
# des.assign_values({ 'time' => 0, 'pressure' => 0.5, 'temperature' => 273.15 })
|
|
171
|
+
def assign_values(values_hash)
|
|
172
|
+
syms.assign_values(values_hash)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Shortcut method to the logger method +add_variables+.
|
|
176
|
+
def add_variables_to_log(vars_decimals)
|
|
177
|
+
logger.add_variables(vars_decimals)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Shortcut method to +Logger#add_all+ log all variables and optionally their rate terms.
|
|
181
|
+
def log_all(decimals, with_terms: false)
|
|
182
|
+
logger.add_all(decimals, with_terms:)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Shortcut method to access the variables of the _syms_ object.
|
|
186
|
+
# @return [Hash{String => DifferentialSystemVariable}] The hash with the variables.
|
|
187
|
+
# @example
|
|
188
|
+
# puts des.variables['time'].value
|
|
189
|
+
def variables = syms.variables
|
|
190
|
+
# Shortcut method to acess the parameters of the _syms_ object.
|
|
191
|
+
# @return [Hash{String => DifferentialSystemParameter}]
|
|
192
|
+
def parameters = syms.parameters
|
|
193
|
+
# Checks if the prepare_integration method has already been called. Because the _current_step_ attribute is only set
|
|
194
|
+
# until the method is called, if it is not nil it means the DES is already set. See {#prepare_integration}
|
|
195
|
+
def ready? = !tracker.current_step.nil?
|
|
196
|
+
# Checks if the adpative method is in use.
|
|
197
|
+
def adaptive? = stepper.butcher_table.adaptive?
|
|
198
|
+
# Shortcut to get the logs
|
|
199
|
+
def logs = logger.logs
|
|
200
|
+
|
|
201
|
+
# The main exposed method to perform the numerical integration through a whole range (duration or timespan).
|
|
202
|
+
# @param starting_values[Hash{String => Numeric}] Provides a last chance to introduce all values to the variables
|
|
203
|
+
# and parameters. See {#assign_values}. The independent variable is set to zero if not provided. Every other
|
|
204
|
+
# variable or parameter with nil value will raise an error.
|
|
205
|
+
# @param step_size[Numeric] Allows to pass the step_size to be used (for adaptive methods, on the first one at
|
|
206
|
+
# least). The step size can be set beforehand.
|
|
207
|
+
# @param duration[Numeric] Allows to pass the duration or timespan of the integration.
|
|
208
|
+
# @param method[Symbol] Specifies a preset method. Currently :rk4, :midpoint and :dopr45 (adpative) are available.
|
|
209
|
+
# It will default to :4rk and :dopr45 for non adaptive and adaptive unless a custom butcher table has been
|
|
210
|
+
# previously set in the _stepper.butcher_table_ object.
|
|
211
|
+
# @param adaptive[Boolean] Chooses if an adpative step size or non adaptive is going to be used.
|
|
212
|
+
# @param display_progress[Boolean] Controls whether a progress bar is displayed in CLI or not.
|
|
213
|
+
def integrate(starting_values: nil, step_size: nil, duration: nil, method: nil, adaptive: false,
|
|
214
|
+
display_progress: true)
|
|
215
|
+
prepare_integration(starting_values:, step_size:, duration:, method:, adaptive:) unless ready?
|
|
216
|
+
progress_bar = Formatador::ProgressBar.new(tracker.total_steps, color: 'light_green') if display_progress
|
|
217
|
+
while syms.ind_var.value < tracker.ending_point
|
|
218
|
+
unitary_step
|
|
219
|
+
progress_bar.increment if display_progress
|
|
220
|
+
end
|
|
221
|
+
self
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Performs only a single step of the integration. Might generate several logging points when using dense output.
|
|
225
|
+
# see {#integrate} for parameters.
|
|
226
|
+
def single_step(starting_values: nil, step_size: nil, duration: nil, method: nil, adaptive: false)
|
|
227
|
+
prepare_integration(starting_values:, step_size:, duration:, method:, adaptive:, single_step: true) unless ready?
|
|
228
|
+
unitary_step
|
|
229
|
+
self
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Converts to JSON object
|
|
233
|
+
# @return [json] The json object.
|
|
234
|
+
def to_json(*_args)
|
|
235
|
+
{ 'variables' => syms.rate_variables.values,
|
|
236
|
+
'parameters' => syms.parameters.values,
|
|
237
|
+
'logging' => logger.entries.values,
|
|
238
|
+
'values' => syms.variables.transform_values(&:value),
|
|
239
|
+
'simulation_options' => tracker }.to_json
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Converts to TOML object. So far there is no satisfactory way to handle null data. Tomlib converts nil to "" so
|
|
243
|
+
# loading it back with _from_toml_ may cause error.
|
|
244
|
+
def to_toml
|
|
245
|
+
Tomlib.dump(JSON.parse(to_json))
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Parses a JSON and loads all its data into the DES object. Useful to store integration metadata and load it later.
|
|
249
|
+
# @return [DifferentialEquationSystem] The DES object itself.
|
|
250
|
+
def from_json(data)
|
|
251
|
+
from_serial(JSON.parse(data))
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# Parses a JSON and loads all its data into the DES object. Useful to store integration metadata and load it later.
|
|
255
|
+
# @return [DifferentialEquationSystem] The DES object itself.
|
|
256
|
+
def from_toml(data)
|
|
257
|
+
from_serial(Tomlib.load(data))
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
# Prints to stdout a summary of the DES.
|
|
261
|
+
def formatador_print
|
|
262
|
+
f = Formatador.new
|
|
263
|
+
f.display_line 'Variables'
|
|
264
|
+
vars = variables.values.reduce([]) do |ar, v|
|
|
265
|
+
ar.push({ symbol: v.symbol, name: v.name, unit: v.unit })
|
|
266
|
+
end
|
|
267
|
+
f.display_compact_table vars, %i[symbol name unit]
|
|
268
|
+
f.display_line 'Parameters'
|
|
269
|
+
params = parameters.values.reduce([]) do |ar, v|
|
|
270
|
+
ar.push({ symbol: v.symbol, name: v.name, unit: v.unit, value: v.value })
|
|
271
|
+
end
|
|
272
|
+
f.display_compact_table params, %i[symbol name value unit]
|
|
273
|
+
f.display_line 'Equations'
|
|
274
|
+
eqs = []
|
|
275
|
+
syms.rate_equations.each do |n, e|
|
|
276
|
+
if e.terms.empty?
|
|
277
|
+
eqs.push({ equation: n, formula: e.rate_function })
|
|
278
|
+
else
|
|
279
|
+
e.terms.each { |sk, sv| eqs.push({ variable: n, term: sk, formula: sv.rate_function }) }
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
f.display_compact_table eqs, %i[variable term formula]
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
private
|
|
286
|
+
|
|
287
|
+
# This is the core internal method that represents the traversing of one step.
|
|
288
|
+
def unitary_step
|
|
289
|
+
walk_step
|
|
290
|
+
append_log
|
|
291
|
+
change_variables
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Adds the independent variable. Kept private to avoid external libraries being able to add independent variables.
|
|
295
|
+
def add_independent(name, symbol = nil, title = nil)
|
|
296
|
+
symbol = name if symbol.nil?
|
|
297
|
+
variables.store name.downcase,
|
|
298
|
+
instance_variable_set("@#{name.downcase}",
|
|
299
|
+
DifferentialSystemVariable.new(name: name, symbol: symbol, title: title,
|
|
300
|
+
independent: true))
|
|
301
|
+
define_singleton_method name.downcase do
|
|
302
|
+
instance_variable_get "@#{name.downcase}"
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# Routine shared by #from_json and #from_toml
|
|
307
|
+
def from_serial(poro)
|
|
308
|
+
poro['variables'].each do |v|
|
|
309
|
+
add_variable v['name'], v['symbol'], v['title'], v['unit'], rate_function: v['rate_function']
|
|
310
|
+
var = variables[v['name']]
|
|
311
|
+
if v['rate_function']
|
|
312
|
+
var.rate_equation.rate_function = v['rate_function']
|
|
313
|
+
else
|
|
314
|
+
v['terms'].each { |h| var.rate_equation.add_rate_term(h['name'], h['rate_function']) }
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
poro['parameters'].each do |p|
|
|
318
|
+
add_parameter p['name'], p['symbol'], p['title'], p['unit'], value: p['value']
|
|
319
|
+
end
|
|
320
|
+
poro['logging'].each do |l|
|
|
321
|
+
case l['type']
|
|
322
|
+
when 'variable'
|
|
323
|
+
logger.add_variables({ l['variable'] => l['decimals'] })
|
|
324
|
+
when 'term'
|
|
325
|
+
logger.add_rate_term l['variable'], l['term'], l['decimals']
|
|
326
|
+
when 'step'
|
|
327
|
+
logger.add_step_state
|
|
328
|
+
when 'expression'
|
|
329
|
+
logger.add_expression l['name'], l['formula'], l['decimals']
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
assign_values poro['values']
|
|
333
|
+
tracker.step_size = poro['simulation_options']['step_size']
|
|
334
|
+
tracker.duration = poro['simulation_options']['duration']
|
|
335
|
+
tracker.logging_interval = poro['simulation_options']['logging_interval']
|
|
336
|
+
self
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Major method to set up all instance variable and objects before starting to perform steps. Single step is
|
|
340
|
+
# included to discriminate whether a duration is necessary or not.
|
|
341
|
+
def prepare_integration(starting_values: nil, step_size: nil, duration: nil, method: nil, adaptive: false,
|
|
342
|
+
single_step: false)
|
|
343
|
+
assign_values({ syms.ind_var.name => 0 }) if syms.ind_var.value.nil?
|
|
344
|
+
assign_values starting_values unless starting_values.nil?
|
|
345
|
+
tracker.step_size = step_size unless step_size.nil?
|
|
346
|
+
tracker.duration = duration unless duration.nil?
|
|
347
|
+
method ||= (adaptive ? :dopr45 : :rk4)
|
|
348
|
+
stepper.prepare(method:)
|
|
349
|
+
tracker.prepare(syms.ind_var.value, stepper.butcher_table.adaptive?, single_step: single_step)
|
|
350
|
+
logger.prepare
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
# Method which calls the major core method which performs a single step.
|
|
354
|
+
def walk_step
|
|
355
|
+
stepper.walk_step tracker.step_size
|
|
356
|
+
tracker.walk_forward
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def change_variables
|
|
360
|
+
variables.each_value { |v| v.value += v.step_change; v.buffer_value = v.value }
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def append_log
|
|
364
|
+
logger.log_forward
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
end
|