monotonic.rb 0.9.0 → 0.10.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02f12d8ccd007cc410e5ebeb524fd6761088aebe771409a81323fd331e479e61
4
- data.tar.gz: 9bfd7ff12760b554b0921e535803c4e3e07c0c0cdb166b8133f1ab710a6a1d10
3
+ metadata.gz: 31e7ee27a90e0a41a51e5739b7024b16a2d7e10f71002bde23362bc90ccef9e5
4
+ data.tar.gz: 48b392a44f8d69bfe6ce2ecaee02b99d47645a148317c97c304188b0584ba3d8
5
5
  SHA512:
6
- metadata.gz: fb3b665160980c7d6319db88518e9bd6f74a5e7eade1035a61e62c6aaa88e06110b382f02900f4e61fc7509cb7d9167084d5650c95858912c4a5b5bb2ffdcd43
7
- data.tar.gz: 9c7c70bb79a05df1608516272583870cd7aab4229e13dce3d626b38fd3426a58c3529bfe2e8ddd2607f3995508c3c1cdcda287212002349db0fabea114a373b5
6
+ metadata.gz: 32e4581c05f382de37da5794488759f1c7206a54f79f1ad593d1809b9a53dde4407a17e4d3ab4eab8b4eaeaa18caea85dfc4300ab219db1e3291bcacb79987a9
7
+ data.tar.gz: 308211ccff21283d9ff5a851396ff660df6f605d3ffe607176c26c53075bfc930f3bbfacc5edfee758f11b6e702d7c5bf46de88e3b3193aa3636ef956dcd7664
data/CHANGELOG CHANGED
@@ -1,5 +1,20 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 20260817
4
+
5
+ 0.10.0: + Monotonic::Measurement arithmetic, so that a speedup is a ratio which knows how well it is known.
6
+
7
+ 1. + Monotonic::Measurement#+ and #-: two intervals give an interval, their doubts combining in quadrature through Measurand rather than by addition. A duration joins as exact, having none of its own, and a bare number is refused, having no unit to be added to.
8
+ 2. + Monotonic::Measurement#*: scales by a number, the uncertainty scaling with it. Two intervals are refused, there being no unit of time squared, as Duration::Common refuses them.
9
+ 3. + Monotonic::Measurement#/: by a number it stays an interval; by another interval the units cancel and a Measurand is left, dimensionless and carrying the doubt of both. That ratio is t1/tn, and the reason for the release: a speedup stated without knowing whether it is real is worth little.
10
+ 4. + Monotonic::Measurement.from: builds from a measurand whose uncertainty has been propagated rather than measured, so that what comes back from arithmetic carries neither an instrument's floor nor a drift still to be applied a second time.
11
+ 5. ~ monotonic.rb.gemspec: /measurand >= 0.1.0/measurand >= 0.1.2/. Measurand#/ divided two integer values as integers until 0.1.2, so a ratio of two nanosecond counts came back as 0 rather than 0.8788. #/ is the point of this release and does not work below 0.1.2, which the added tests demonstrate: they fail twice against 0.1.0 and pass against 0.1.2.
12
+ 6. + test/Monotonic/Measurement_test.rb: each operator and what it refuses, and the ratio — that it is dimensionless, that it divides exactly, and that it carries the doubt of both operands.
13
+ 7. + test/gemspec_test.rb: that each runtime dependency declares a minimum, and which minimum. The existing test asserted the names alone, which is what let 5 above happen: the gemspec named measurand and said nothing about needing a measurand that works. Asserting the whole map means a minimum cannot move without the test being made to agree.
14
+ 8. ~ README.md: + the arithmetic to the Description, + a Monotonic::Measurement arithmetic section to Usage, and + a blank line after require in two examples, the other three having had one.
15
+ 9. ~ Monotonic::VERSION: /0.9.0/0.10.0/
16
+
17
+
3
18
  ## 20260816
4
19
 
5
20
  0.9.0: Use measurand gem, + Monotonic::Measurement and Monotonic::Timer.floor.
data/README.md CHANGED
@@ -51,6 +51,15 @@ measurement.to_duration.to_microseconds.to_f
51
51
 
52
52
  Nothing is capped: a day resolved to a hundred nanoseconds prints every figure it has, the absurdity being the mismatch between what the instrument resolves and what was asked of it, which is better seen than rounded away. The floor is a floor and not an error bar — the tail is unbounded and one-sided, the scheduler being free to take the processor away between the two readings — so repeat and take a robust statistic where that matters.
53
53
 
54
+ Measurements combine, and what may combine with what follows from the units. Two intervals add and subtract to an interval, their doubts going in quadrature; a duration joins in as exact, having none of its own; a bare number is refused, having no unit to be added to. Scaling by a number keeps the unit and scales the doubt with it, while multiplying two intervals is refused, there being no unit of time squared. Dividing one interval by another cancels the units and leaves a `Measurand` — dimensionless, and still knowing how well it is known, which is what a speedup is.
55
+
56
+ ```ruby
57
+ serial = Monotonic::Timer.time{run_on_one}
58
+ parallel = Monotonic::Timer.time{run_on_eight}
59
+ serial / parallel
60
+ # => Measurand(6.83, 0.00012), dimensionless
61
+ ```
62
+
54
63
 
55
64
  ## Installation
56
65
 
@@ -143,6 +152,7 @@ measurement.to_s
143
152
 
144
153
  ```ruby
145
154
  require 'monotonic.rb'
155
+
146
156
  measurement = Monotonic::Timer.time do |timer|
147
157
  i = 0
148
158
  500_000.times{puts i += 1}
@@ -157,6 +167,7 @@ measurement.to_s
157
167
 
158
168
  ```ruby
159
169
  require 'monotonic.rb'
170
+
160
171
  timer = Monotonic::Timer.new
161
172
  measurement = timer.time do
162
173
  i = 0
@@ -167,6 +178,34 @@ measurement.to_s
167
178
  ```
168
179
 
169
180
 
181
+ ### Monotonic::Measurement arithmetic
182
+
183
+ ```ruby
184
+ require 'monotonic.rb'
185
+
186
+ a = Monotonic::Timer.time{sleep 0.001}
187
+ b = Monotonic::Timer.time{sleep 0.002}
188
+
189
+ (a + b).to_s
190
+ # => "3.01543 ms"
191
+
192
+ (a * 3).to_s
193
+ # => "3.04629 ms"
194
+
195
+ a + Duration::Microseconds.new(500)
196
+ # => #<Monotonic::Measurement 1.51543 ms>, a duration being exact
197
+
198
+ b / a
199
+ # => Measurand(1.97, 0.00011), dimensionless
200
+
201
+ a * b
202
+ # => TypeError: there is no unit of time squared
203
+
204
+ a + 5
205
+ # => TypeError: expected an interval or a duration
206
+ ```
207
+
208
+
170
209
  ## Contributing
171
210
 
172
211
  1. Fork it ( https://github.com/thoran/monotonic.rb/fork )
@@ -23,6 +23,16 @@ module Monotonic
23
23
  # not divide by ten.
24
24
  UNITS = [[1, 'ns'], [1_000, 'us'], [1_000_000, 'ms'], [1_000_000_000, 's']]
25
25
 
26
+ class << self
27
+ # Built from a measurand rather than from a floor, for what comes back
28
+ # from arithmetic: its uncertainty has been propagated rather than
29
+ # measured, so it is neither an instrument's floor nor has a drift left to
30
+ # apply a second time.
31
+ def from(measurand)
32
+ allocate.tap{|measurement| measurement.send(:build, measurand)}
33
+ end
34
+ end # class << self
35
+
26
36
  attr_reader :nanoseconds
27
37
  attr_reader :drift
28
38
 
@@ -51,6 +61,35 @@ module Monotonic
51
61
  measurand.uncertainty
52
62
  end
53
63
 
64
+ # The arithmetic is Measurand's throughout — this only decides what may be
65
+ # combined with what, and hands back the right kind of thing. Two intervals
66
+ # add and subtract to an interval, their uncertainties going in quadrature.
67
+ def +(addend)
68
+ self.class.from(measurand + as_measurand(addend, :add))
69
+ end
70
+
71
+ def -(subtrahend)
72
+ self.class.from(measurand - as_measurand(subtrahend, :subtract))
73
+ end
74
+
75
+ # Scaled by a number it stays an interval, the uncertainty scaling with it.
76
+ # Multiplied by another interval it would be time squared, which has no unit
77
+ # here, so it is refused as Duration::Common refuses it.
78
+ def *(multiplier)
79
+ if interval?(multiplier)
80
+ raise TypeError, "can't multiply #{self.class} by #{multiplier.class}: there is no unit of time squared"
81
+ end
82
+ self.class.from(measurand * multiplier)
83
+ end
84
+
85
+ # Divided by a number it stays an interval. Divided by another interval the
86
+ # units cancel and a Measurand is left — dimensionless, but still knowing how
87
+ # well it is known, which is what a speedup or a rate is.
88
+ def /(divisor)
89
+ return measurand / divisor.measurand if divisor.respond_to?(:measurand)
90
+ self.class.from(measurand / divisor)
91
+ end
92
+
54
93
  def unit
55
94
  @unit ||= UNITS.reverse.find{|scale, _| @nanoseconds.abs >= scale} || UNITS.first
56
95
  end
@@ -114,5 +153,24 @@ module Monotonic
114
153
  @floor = floor
115
154
  @drift = drift
116
155
  end
156
+
157
+ # An interval is anything which knows its own doubt, or any duration, which
158
+ # is exact. A bare number is neither: it has no unit, so there is nothing to
159
+ # add it to.
160
+ def as_measurand(other, verb)
161
+ return other.measurand if other.respond_to?(:measurand)
162
+ return Measurand.new(other.to_nanoseconds.to_i) if other.respond_to?(:to_nanoseconds)
163
+ raise TypeError, "can't #{verb} #{other.class} #{verb == :add ? "to" : "from"} #{self.class}: expected an interval or a duration"
164
+ end
165
+
166
+ def interval?(other)
167
+ other.respond_to?(:measurand) || other.respond_to?(:to_nanoseconds)
168
+ end
169
+
170
+ def build(measurand)
171
+ @nanoseconds = measurand.value.round
172
+ @measurand = measurand
173
+ @floor = measurand.uncertainty
174
+ end
117
175
  end
118
176
  end
@@ -2,5 +2,5 @@
2
2
  # Monotonic::VERSION
3
3
 
4
4
  module Monotonic
5
- VERSION = '0.9.0'
5
+ VERSION = '0.10.0'
6
6
  end
data/monotonic.rb.gemspec CHANGED
@@ -37,7 +37,7 @@ Gem::Specification.new do |spec|
37
37
 
38
38
  spec.dependencies = [
39
39
  ['duration.rb', '>= 0.4.0'],
40
- ['measurand', '>= 0.1.0'],
40
+ ['measurand', '>= 0.1.2'],
41
41
  'sys-uptime'
42
42
  ]
43
43
 
@@ -96,6 +96,67 @@ describe Monotonic::Measurement do
96
96
  end
97
97
  end
98
98
 
99
+ describe "arithmetic" do
100
+ it "adds two intervals to an interval, the doubt going in quadrature" do
101
+ sum = measurement(1_000_000) + measurement(2_000_000)
102
+ _(sum).must_be_instance_of(Monotonic::Measurement)
103
+ _(sum.nanoseconds).must_equal(3_000_000)
104
+ _(sum.uncertainty).must_be_close_to(Math.sqrt(84**2 + 84**2), 0.0001)
105
+ end
106
+
107
+ it "subtracts likewise, the doubt growing rather than cancelling" do
108
+ difference = measurement(2_000_000) - measurement(1_000_000)
109
+ _(difference.nanoseconds).must_equal(1_000_000)
110
+ _(difference.uncertainty).must_be_close_to(Math.sqrt(84**2 + 84**2), 0.0001)
111
+ end
112
+
113
+ it "takes a duration as exact, it having no doubt of its own" do
114
+ sum = measurement(1_000_000) + Duration::Microseconds.new(500)
115
+ _(sum.nanoseconds).must_equal(1_500_000)
116
+ _(sum.uncertainty).must_equal(84)
117
+ end
118
+
119
+ it "refuses a bare number, which has no unit to add" do
120
+ _{measurement(1_000_000) + 5}.must_raise(TypeError)
121
+ _{measurement(1_000_000) - 5}.must_raise(TypeError)
122
+ end
123
+
124
+ it "scales by a number, the uncertainty scaling with it" do
125
+ product = measurement(1_000_000) * 3
126
+ _(product.nanoseconds).must_equal(3_000_000)
127
+ _(product.uncertainty).must_equal(252)
128
+ end
129
+
130
+ it "refuses to multiply two intervals, there being no unit of time squared" do
131
+ _{measurement(1_000_000) * measurement(2_000_000)}.must_raise(TypeError)
132
+ _{measurement(1_000_000) * Duration::Seconds.new(1)}.must_raise(TypeError)
133
+ end
134
+
135
+ it "divides by a number and stays an interval" do
136
+ quotient = measurement(1_000_000) / 2
137
+ _(quotient).must_be_instance_of(Monotonic::Measurement)
138
+ _(quotient.nanoseconds).must_equal(500_000)
139
+ _(quotient.uncertainty).must_equal(42)
140
+ end
141
+ end
142
+
143
+ describe "the ratio of two intervals" do
144
+ it "is dimensionless, the units having cancelled" do
145
+ ratio = measurement(9_579_583) / measurement(10_900_458)
146
+ _(ratio).must_be_instance_of(Measurand)
147
+ _(ratio.value.to_f).must_be_close_to(0.878824, 0.000001)
148
+ end
149
+
150
+ it "divides exactly rather than as integers, which measurand 0.1.2 settled" do
151
+ _((measurement(1_000_000) / measurement(3_000_000)).value).must_equal(Rational(1, 3))
152
+ end
153
+
154
+ it "carries the doubt of both, which is what a speedup is worth knowing" do
155
+ ratio = measurement(9_579_583) / measurement(10_900_458)
156
+ _(ratio.uncertainty).must_be_close_to(1.026e-05, 1e-8)
157
+ end
158
+ end
159
+
99
160
  describe "#to_duration" do
100
161
  it "hands the interval to duration.rb, whose business the units are" do
101
162
  _(measurement(1_500_000_000).to_duration).must_be_instance_of(Duration::Nanoseconds)
data/test/gemspec_test.rb CHANGED
@@ -22,6 +22,16 @@ describe 'monotonic.rb.gemspec' do
22
22
  _(spec.runtime_dependencies.map(&:name).sort).must_equal(%w{duration.rb measurand sys-uptime})
23
23
  end
24
24
 
25
+ # The minimum is what makes a dependency true, and naming the gem alone does
26
+ # not say that it will work. 0.9.0 constrained measurand to >= 0.1.0, where
27
+ # #place became public; #/ then became load-bearing at 0.1.2 without the
28
+ # constraint following it, and Measurement#/ returned 0 against a version the
29
+ # gemspec called satisfactory.
30
+ it "declares a minimum version for each runtime dependency" do
31
+ _(spec.runtime_dependencies.to_h{|dependency| [dependency.name, dependency.requirement.to_s]}) \
32
+ .must_equal({'duration.rb' => '>= 0.4.0', 'measurand' => '>= 0.1.2', 'sys-uptime' => '>= 0'})
33
+ end
34
+
25
35
  it "declares its development dependencies" do
26
36
  _(spec.development_dependencies.map(&:name).sort).must_equal(%w{minitest minitest-spec-context rake})
27
37
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monotonic.rb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - thoran
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 0.1.0
32
+ version: 0.1.2
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 0.1.0
39
+ version: 0.1.2
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: sys-uptime
42
42
  requirement: !ruby/object:Gem::Requirement