Class: YPetri::Net::DataSet

Inherits:
Hash
  • Object
show all
Defined in:
lib/y_petri/net/data_set.rb

Overview

YPetri::Net::DataSet is a collection of labeled state records. It is a subclass of Hash. DataSet keys are known as events, and values are data points. Each of these data points is an array that can be used to reconstruct a record, (an object of YPetri::Net::State::Features::Record class). Each record records a specific feature set (of YPetri::Net::State::Features class). DataSet class is intended to be parametrized with such specific feature set, which indicates the meaning of its data points.

Apart from methods inherited from Hash, DataSet can load a record at a given event (#record method), reconstruct a simulation at a given event (#reconstruct method), return columns corresponding to features (#series method) and perform feature selection (#marking, #firing, #flux, #gradient, #delta, #assignment, and #reduced_features for mixed feature sets). Apart from standard inspection methods, DataSet has methods #print and #plot for visual presentation. Also, DataSet has methods specially geared towards records of timed simulations, whose events are points in time. Method #interpolate uses linear interpolation to find the approximate state of the system at some exact time using linear interpolation between the nearest earlier and later data points (which can be accessed respectively by #floor and #ceiling methods). Interpolation is available to the user, and is also used by DataSet#resample method for resampling the dataset.

Finally, it is possible that especially professional statisticians have written, or are planning to write, a DataSet class better than this one. If I discover a good DataSet class in the future, I would like to inherit from it or otherwise integrate with it for the purposes of DataSet.

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#settingsObject (readonly)

TODO: More like event_type, idea not matured yet.


77
78
79
# File 'lib/y_petri/net/data_set.rb', line 77

def settings
  @settings
end

#typeObject (readonly)

TODO: More like event_type, idea not matured yet.


77
78
79
# File 'lib/y_petri/net/data_set.rb', line 77

def type
  @type
end

Class Method Details

.__new__Object


54
# File 'lib/y_petri/net/data_set.rb', line 54

alias __new__ new

.new(type: nil) ⇒ Object


56
57
58
59
60
61
62
63
64
# File 'lib/y_petri/net/data_set.rb', line 56

def new type: nil
  __new__ do |hsh, missing|
    case missing
    when Float then nil
    else hsh[ missing.to_f ] end
  end.tap { |inst|
    inst.instance_variable_set :@type, type
  }
end

Instance Method Details

#assignment(*ids) ⇒ Object

Expects an arbitrary number of assignment feature identifiers as arguments, and returns a subset of this dataset with only the specified assignment features retained. If no arguments are given, all the assignment features are assumed.


355
356
357
358
# File 'lib/y_petri/net/data_set.rb', line 355

def assignment *ids
  return reduce_features net.State.Features.assignment if args.empty?
  reduce_features assignment: ids
end

#Assignment(array) ⇒ Object

Expects an array of assignment feature identifiers. Returns a subset of this dataset with only the specified assignment features retained.


346
347
348
# File 'lib/y_petri/net/data_set.rb', line 346

def Assignment array
  reduce_features assignment: array
end

#ceiling(event, equal_ok = true) ⇒ Object

Returns the nearest event greater or equal to the supplied event-type argument. The second optional ordered argument, true by default, controls whether equality is accepted. If set to false, then the nearest greater event is sought.


107
108
109
110
# File 'lib/y_petri/net/data_set.rb', line 107

def ceiling( event, equal_ok=true )
  e = events.ascending_ceiling( event, equal_ok )
  e.nil? ? nil : e
end

#Delta(array, transitions: nil, **named_args) ⇒ Object

Expects an array of delta feature identifiers, optionally qualified by the :transitions named argument, defaulting to all the transitions in the net.


306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
# File 'lib/y_petri/net/data_set.rb', line 306

def Delta array, transitions: nil, **named_args
  if named_args.has? :delta_time, syn!: :Δt then
    Δt = named_args.delete( :delta_time )
    if transitions.nil? then
      reduce_features delta: array, Δt: Δt
    else
      reduce_features delta: [ *array, transitions: transitions ], Δt: Δt
    end
  else
    if transitions.nil? then
      reduce_features delta: array
    else
      reduce_features delta: [ *array, transitions: transitions ]
    end
  end
end

#delta(*ordered_args, transitions: nil, **named_args) ⇒ Object

Expects an arbitrary number of ordered arguments identifying delta features, optionally qualified by the :transitions named argument, defaulting to all the transitions in the net.


327
328
329
330
331
332
# File 'lib/y_petri/net/data_set.rb', line 327

def delta *ordered_args, transitions: nil, **named_args
  return Delta( ordered_args, transitions: transitions, **named_args ) unless
    ordered_args.empty?
  return Delta( net.places, **named_args ) if transitions.nil?
  Delta( net.places, transitions: transitions, **named_args )
end

#delta_timed(*ordered_args, **named_args) ⇒ Object


335
336
337
# File 'lib/y_petri/net/data_set.rb', line 335

def delta_timed *ordered_args, **named_args
  delta *ordered_args, transitions: net.T_transitions, **named_args
end

#delta_timeless(*ordered_args, **named_args) ⇒ Object


339
340
341
# File 'lib/y_petri/net/data_set.rb', line 339

def delta_timeless *ordered_args, **named_args
  delta *ordered_args, transitions: net.t_transitions, **named_args
end

#distance(other) ⇒ Object

Computes the distance to another dataset.


171
172
173
174
175
176
177
178
# File 'lib/y_petri/net/data_set.rb', line 171

def distance( other )
  sum_of_sq = events
    .map { |e| [ e, other.interpolate( e ) ] }
    .map { |rec1, rec2| rec1.euclidean_distance rec2 }
    .map { |dist| dist * dist }
    .reduce( :+ )
  sum_of_sq ** 0.5
end

#Firing(array, **named_args) ⇒ Object

Expects an array of firing feature identifiers, and returns a subset of this dataset with only the specified firing features retained. Named arguments may include :delta_time, alias :Δt (for firing of timed transitions).


250
251
252
# File 'lib/y_petri/net/data_set.rb', line 250

def Firing array, **named_args
  reduce_features firing: array, **named_args
end

#firing(*ids, **named_args) ⇒ Object

Expects an arbitrary number of firing feature identifiers and returns a subset of this dataset with only the specified firing features retained. Named arguments may include :delta_time, alias :Δt (for firing of timed transitions).


259
260
261
262
# File 'lib/y_petri/net/data_set.rb', line 259

def firing *ids, **named_args
  return Firing net.State.Features.firing, **named_args if ids.empty?
  Firing ids, **named_args
end

#floor(event, equal_ok = true) ⇒ Object

Returns the nearest event smaller or equal to the supplied event-type argument. The second optional ordered argument, true by default, controls whether equality is accepted. If set to false, then the nearest smaller event is sought.


97
98
99
100
# File 'lib/y_petri/net/data_set.rb', line 97

def floor( event, equal_ok=true )
  e = events.ascending_floor( event, equal_ok )
  e.nil? ? nil : e
end

#flux(*ids) ⇒ Object

Expects an arbitrary number of flux feature identifiers, and returns a subset of this dataset, with only the specified flux features retained. If no aruments are given, full set of flux features is assumed.


275
276
277
278
# File 'lib/y_petri/net/data_set.rb', line 275

def flux *ids
  return Flux net.State.Features.flux if ids.empty?
  Flux ids
end

#Flux(array) ⇒ Object

Expects an array of flux feature identifiers, and returns a subset of this dataset with only the specified flux features retained.


267
268
269
# File 'lib/y_petri/net/data_set.rb', line 267

def Flux array
  reduce_features flux: array
end

#Gradient(array, transitions: nil) ⇒ Object

Expects an array of gradient feature identifiers, optionally qualified by the :transitions named argument, defaulting to all T transitions in the net.


284
285
286
287
288
289
290
# File 'lib/y_petri/net/data_set.rb', line 284

def Gradient array, transitions: nil
  if transitions.nil? then
    reduce_features gradient: array
  else
    reduce_features gradient: [ *array, transitions: transitions ]
  end
end

#gradient(*ids, transitions: nil) ⇒ Object

Returns a subset of this dataset with only the specified gradient features identified by the arguments retained. If no arguments are given, all the gradient features from the receiver dataset are selected.


296
297
298
299
300
# File 'lib/y_petri/net/data_set.rb', line 296

def gradient *ids, transitions: nil
  return Gradient net.State.Features.gradient, transitions: transitions if
    ids.empty?
  Gradient ids, transitions: transitions
end

#inspectObject

Inspect string of the instance.


468
469
470
# File 'lib/y_petri/net/data_set.rb', line 468

def inspect
  to_s
end

#interpolate(event) ⇒ Object Also known as: at

Interpolates the recording at the given point (event). Return value is the Record class instance.


134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
# File 'lib/y_petri/net/data_set.rb', line 134

def interpolate( event )
  begin
    record( event )
  rescue KeyError => msg
    timed? or raise TypeError, "Event #{event} not recorded! (%s)" %
      "simulation type: #{type.nil? ? 'nil' : type}"    # (Remark: #floor, #ceiling supported by timed datasets only)

    floor = floor( event )
    fail "Event #{event} has no floor!" if floor.nil?
    fl = Matrix.column_vector record( floor )
    ceiling = ceiling( event )
    fail "Event #{event} has no ceiling!" if ceiling.nil?
    ce = Matrix.column_vector record( ceiling )
    rslt = fl + ( ce - fl ) / ( ceiling - floor ) * ( event - floor )
    features.load( rslt.column_to_a )
  end
end

#marking(*ids) ⇒ Object

Expects an arbitrary number of marking feature identifiers, and returns a subset of this dataset with only the specified marking features retained. If no arguments are given, all the marking features are assumed.


240
241
242
243
# File 'lib/y_petri/net/data_set.rb', line 240

def marking *ids
  return Marking net.State.Features.marking if ids.empty?
  Marking ids
end

#Marking(array) ⇒ Object

Expects an array of marking feature identifiers, and returns a subset of this dataset with only the specified marking features retained.


232
233
234
# File 'lib/y_petri/net/data_set.rb', line 232

def Marking array
  reduce_features marking: array
end

#plot(nodes = nil, except: [], **named_args) ⇒ Object

Plots the dataset using Ruby Gnuplot gem. Takes several optional arguments: The list of nodes to plot can be supplied as optional first ordered argument. If supplied, the nodes are converted into features using Net::State::Features.infer_from_nodes method. Similarly, the features to exclude can be specified as a list of nodes supplied under except: parameter. Under except: parameter, it is also possible to supply a feature-specifying hash. Otherwise, feature specification can be passed to the method via named arguments. By default, it is assumed that the caller means to plot all the features of this dataset.

Named arguments admit Gnuplot keywords that control the plot. These parameters include title:, xlabel: and ylabel:.


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
# File 'lib/y_petri/net/data_set.rb', line 385

def plot( nodes=nil, except: [], **named_args )
  nn = named_args
  time = nn.may_have :time, syn!: :time_range
  events = events()  # Figure out the features to plot.

  ff = if nodes.nil? then
         nn_ff = nn.slice [ :marking, :flux, :firing,
                            :gradient, :delta, :assignment ]
         nn_ff.empty? ? features : net.State.Features( nn_ff )
       else
         net.State.Features.infer_from_nodes( nodes )
       end  # Figure out the features to exclude from plotting
  # ("except" features).

  xff = case except
        when Array then net.State.Features.infer_from_nodes( except )
        when Hash then net.State.Features( except )
        else
          fail TypeError, "Wrong type of :except argument: #{except.class}"
        end  # Subtract the "except" features from features to plot.

  ff -= xff  # Convert the feature set into a set of data arrays.

  data_arrays = series( ff )  # Figure out the x axis range for plotting.

  x_range = if nn.has? :time then
              if time.is_a? Range then
                "[#{time.begin}:#{time.end}]"
              else
                "[-0:#{SY::Time.magnitude( time ).amount rescue time}]"
              end
            else
              from = events.first || 0
              to = if events.last and events.last > from then events.last
                   else events.first + 1 end
              "[#{from}:#{to}]"
            end  # Invoke Gnuplot.

  Gnuplot.open do |gp|
    Gnuplot::Plot.new gp do |plot|
      plot.xrange x_range
      if nn.has? :yrange, syn!: :y_range then
        if nn[:yrange].is_a? Range then
          plot.yrange "[#{nn[:yrange].begin}:#{nn[:yrange].end}]"
        else fail TypeError, "Argument :yrange is not a range!" end
      end
      plot.title nn[:title] || "#{net} plot"
      plot.ylabel nn[:ylabel] || "Values"
      plot.xlabel nn[:xlabel] || "Time [s]"
      ff.labels.zip( data_arrays ).each do |label, array|
        # Replace NaN and Infinity with 0.0 and warn about it.
        nan, inf = 0, 0
        array = array.map { |v|
          if v.to_f.infinite? then inf += 1; 0.0
          elsif v.to_f.nan? then nan += 1; 0.0
          else v end
        }        # Warn.

        nan = nan > 0 ? "#{nan} NaN values" : nil
        inf = inf > 0 ? "#{inf} infinite values" : nil
        msg = "Warning: column #{label} contains %s plotted as 0!"
        warn msg % [ nan, inf ].compact.join( ' and ' ) if nan or inf        # Finally, plot.

        plot.data << Gnuplot::DataSet.new( [ events, array ] ) { |set|
          set.with = "linespoints"
          set.title = label
        }
      end
    end
  end
end

Pretty print the dataset. Takes :precision and :distance named arguments, that control the shape of the printed table.


475
476
477
478
479
480
481
482
# File 'lib/y_petri/net/data_set.rb', line 475

def print precision: 4, distance: precision + 4
  features.labels.print_as_line precision: precision, distance: distance
  puts '-' * features.size * distance
  records.each { |record|
    record.print_as_line precision: precision, distance: distance
  }
  return nil
end

#reconstruct(at: (fail "No event given!"), **settings) ⇒ Object

Recreates the simulation at a given event label.


120
121
122
123
124
125
126
127
128
129
# File 'lib/y_petri/net/data_set.rb', line 120

def reconstruct at: (fail "No event given!"), **settings  # settings may include marking clamps, marking, inital marking...

  record = interpolate( at )
  settings = settings().merge settings if settings()
  if timed? then
    record.reconstruct time: at, **settings
  else
    record.reconstruct **settings
  end
end

#record(event) ⇒ Object

Returns the Record instance corresponding to the given recorded event.


88
89
90
# File 'lib/y_petri/net/data_set.rb', line 88

def record( event )
  features.load( fetch event )
end

#recordsObject

Revives records from values.


114
115
116
# File 'lib/y_petri/net/data_set.rb', line 114

def records
  values.map { |value| features.Record.new( value ) }
end

#reduce_features(array = nil, **named_args) ⇒ Object

Expects a hash of features (:marking (alias :state) of places, :firing of tS transitions, :delta of places and/or transitions, :assignment of A transitions) and returns the corresponding mapping of the recording.


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
# File 'lib/y_petri/net/data_set.rb', line 191

def reduce_features array=nil, **named_args
  delta_time_given = named_args.has? :delta_time, syn!: :Δt
  Δt = named_args.delete :delta_time
  ff = net.State.Features[ *array, **named_args ] # reduced feature set
  absent = ff - features()         # features absent from the current set
  present = ff - absent            # features present in the current set
  timedness = true if absent.any? { |f| f.timed? rescue false }
  fail ArgumentError, "Reconstruction of timed features requires Δt to be" +
    "supplied!" unless delta_time_given if timedness
  present_ii =
    present.each_with_object( {} ) { |f, | [f] = features().index f }
  ds = ff.DataSet.new type: type
  if absent.empty? then # no reconstruction
    ( events >> records ).each_with_object ds do |(event, record), dataset|
      line = record.values_at *ff.map( &present_ii.method( :[] ) )
      dataset.update event => ff.load( line )
    end
  else
    ( events >> records ).each_with_object ds do |(event, record), dataset|
      reconstructed_sim = reconstruct at: event
      line = if timedness then
               ff.map { |f|
                 i = present_ii[ f ]
                 break record[ i ] if i
                 f.extract_from( reconstructed_sim, Δt: Δt )
               }
             else
               ff.map { |f|
                 i = present_ii[ f ]
                 break record[ i ] if i
                 f.extract_from( reconstructed_sim, Δt: Δt )
               }
             end
      dataset.update event => ff.load( line )
    end
  end
end

#resample(**settings) ⇒ Object

Resamples the recording.


155
156
157
158
159
160
161
162
163
164
165
166
167
# File 'lib/y_petri/net/data_set.rb', line 155

def resample **settings
  time_range = settings.may_have( :time_range, syn!: :time ) ||
    events.first .. events.last
  sampling = settings.must_have :sampling
  t0, target_time = time_range.begin, time_range.end
  t = t0
  o = self.class.new type: type
  loop do
    o.update t => interpolate( t )
    t += sampling
    return o if t > target_time
  end
end

#series(array = nil) ⇒ Object

Returns the data series for the specified features.


182
183
184
185
# File 'lib/y_petri/net/data_set.rb', line 182

def series array=nil
  return records.transpose if array.nil?
  reduce_features( net.State.Features array ).series
end

#timed?Boolean

Type of the dataset.


82
83
84
# File 'lib/y_petri/net/data_set.rb', line 82

def timed?
  type == :timed
end

#to_csvObject

Outputs the current recording in CSV format.


362
363
364
365
366
# File 'lib/y_petri/net/data_set.rb', line 362

def to_csv
  require 'csv'
  [ ":event", *features.labels.map( &:to_s ) ].join( ',' ) + "\n" +
    map { |lbl, rec| [ lbl, *rec ].join ',' }.join( "\n" )
end

#to_sObject

Returns a string briefly discribing the dataset.


459
460
461
462
463
464
# File 'lib/y_petri/net/data_set.rb', line 459

def to_s
  "#<DataSet: " +
    "#{keys.size} records, " +
    "features: #{features}" +
    ">"
end