Class: Syskit::Models::SpecializationManager

Inherits:
Object
  • Object
show all
Extended by:
MetaRuby::Attributes
Defined in:
lib/syskit/models/specialization_manager.rb

Overview

Management of the specializations of a particular composition model

Defined Under Namespace

Classes: SpecializationBlockContext

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(composition_model) ⇒ SpecializationManager

Returns a new instance of SpecializationManager



14
15
16
# File 'lib/syskit/models/specialization_manager.rb', line 14

def initialize(composition_model)
    @composition_model = composition_model
end

Instance Attribute Details

#composition_modelModel<Composition> (readonly)

The composition model

Returns:



10
11
12
# File 'lib/syskit/models/specialization_manager.rb', line 10

def composition_model
  @composition_model
end

Instance Method Details

#add_specialization_constraint(explicit = nil) {|spec0, spec1| ... } ⇒ void

This method returns an undefined value.

Registers a block that will be able to tell the system that two specializations are not compatible (i.e. should never be applied at the same time).

Parameters:

  • a (#[])

    proc object given explicitly if the block form is not desired

Yield Parameters:

Yield Returns:

  • (Boolean)

    true if the two specializations are compatible, and false otherwise



252
253
254
# File 'lib/syskit/models/specialization_manager.rb', line 252

def add_specialization_constraint(explicit = nil, &as_block)
    specialization_constraints << (explicit || as_block)
end

#all_default_specializationArray<Object>

The union, along the class hierarchy, of all the values stored in default_specialization

Returns:



12
# File 'lib/syskit/models/specialization_manager.rb', line 12

inherited_attribute(:default_specialization, :default_specializations, :map => true) { Hash.new }

#compatible_specializations?(spec1, spec2) ⇒ Boolean

Returns true if the two given specializations are compatible, as given by the registered specialization constraints

This method also checks that the values returned by the constraints are symmetric

Returns:

  • (Boolean)

    false if at least one specialization constraint block returns false, and true otherwise

Raises:

See Also:



268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/syskit/models/specialization_manager.rb', line 268

def compatible_specializations?(spec1, spec2)
    specialization_constraints.all? do |validator|
        # This is potentially expensive, but the specialization
        # compatibilities are done at modelling time, so that's not
        # an issue given the added robustness -- designing properly
        # symmetric constraint blocks can be a bit tricky
        result = validator[spec1, spec2]
        sym_result = validator[spec2, spec1]
        if result != sym_result
            raise NonSymmetricSpecializationConstraint.new(validator, [spec1, spec2]), "#{validator} returned #{!!result} on (#{spec1},#{spec2}) and #{!!sym_result} on (#{spec2},#{spec1}). Specialization constraints must be symmetric"
        end
        result
    end
end

#create_specialized_model(composite_spec, applied_specializations) ⇒ Object



441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
# File 'lib/syskit/models/specialization_manager.rb', line 441

def create_specialized_model(composite_spec, applied_specializations)
    # There's no composition with that spec. Create a new one
    child_composition = composition_model.new_specialized_submodel
    child_composition.private_model
    child_composition.root_model = composition_model.root_model

    child_composition.specialized_children.merge!(composite_spec.specialized_children)
    child_composition.applied_specializations = applied_specializations.to_set
    composite_spec.compatibilities.each do |single_spec|
        child_composition.specializations.register(single_spec)
    end
    composite_spec.specialized_children.each do |child_name, child_models|
        child_composition.overload child_name, child_models
    end

    applied_specializations.each do |applied_spec|
        applied_spec.specialization_blocks.each do |block|
            reference_model =
                if applied_spec == composite_spec
                    child_composition
                else
                    applied_spec.composition_model
                end

            context = SpecializationBlockContext.new(child_composition, reference_model)
            context.apply_block(block)
        end
    end
    child_composition
end

#default_specialization(child, child_model) ⇒ Object

Declares a preferred specialization in case two specializations match that are not related to each other.

In the following case:

composition 'ManualDriving' do
  specialize 'Control', SimpleController, :not => FourWheelController do
  end
  specialize 'Control', FourWheelController, :not => SimpleController do
  end
end

If a Control model is selected that fullfills both SimpleController and FourWheelController, then there is an ambiguity as both specializations apply and one cannot be preferred w.r.t. the other.

By using

default_specialization 'Control', SimpleController

the first one will be preferred by default. The second one can then be selected at instanciation time with

add 'ManualDriving',
    'Control' => controller_model.as(FourWheelController)

Raises:

  • (NotImplementedError)


308
309
310
311
312
313
314
315
316
317
# File 'lib/syskit/models/specialization_manager.rb', line 308

def default_specialization(child, child_model)
    raise NotImplementedError

    child = if child.respond_to?(:to_str)
                child.to_str
            else child.name.gsub(/.*::/, '')
            end

    default_specializations[child] = child_model
end

#default_specializationsObject



12
# File 'lib/syskit/models/specialization_manager.rb', line 12

inherited_attribute(:default_specialization, :default_specializations, :map => true) { Hash.new }

#deregister(specialization) ⇒ void

This method returns an undefined value.

Deregisters the given specialization on this manager

Parameters:



37
38
39
40
41
42
43
# File 'lib/syskit/models/specialization_manager.rb', line 37

def deregister(specialization)
    if specializations[specialization.specialized_children] == specialization
        instanciated_specializations.delete(specialization.specialized_children)
        specializations.delete(specialization.specialized_children)
        composition_model.deregister_submodels([specialization.composition_model].to_set)
    end
end

#each_default_specialization(key, uniq = true) {|element| ... } ⇒ Object #each_default_specialization(nil, uniq = true) {|key, element| ... } ⇒ Object

Overloads:

  • #each_default_specialization(key, uniq = true) {|element| ... } ⇒ Object

    Enumerates all objects registered in default_specialization under the given key

    Yields:

    • (element)

    Yield Parameters:

  • #each_default_specialization(nil, uniq = true) {|key, element| ... } ⇒ Object

    Enumerates all objects registered in default_specialization

    Yields:

    • (key, element)

    Yield Parameters:



12
# File 'lib/syskit/models/specialization_manager.rb', line 12

inherited_attribute(:default_specialization, :default_specializations, :map => true) { Hash.new }

#each_specialization {|CompositionSpecialization| ... } ⇒ Object

Enumerates all specializations defined on #composition_model



55
56
57
58
59
60
# File 'lib/syskit/models/specialization_manager.rb', line 55

def each_specialization
    return enum_for(:each_specialization) if !block_given?
    specializations.each_value do |spec|
        yield(spec)
    end
end

#empty?Boolean

Returns true if no specializations are registered on this manager

Returns:

  • (Boolean)


48
49
50
# File 'lib/syskit/models/specialization_manager.rb', line 48

def empty?
    specializations.empty?
end

#find_common_specialization_subset(candidates) ⇒ Object

Given a set of specialization sets, returns subset common to all of the contained sets



657
658
659
660
661
662
663
664
665
666
667
# File 'lib/syskit/models/specialization_manager.rb', line 657

def find_common_specialization_subset(candidates)
    result = candidates[0][1].to_set
    candidates[1..-1].each do |merged, subset|
        result &= subset.to_set
    end

    merged = result.inject(CompositionSpecialization.new) do |merged, spec|
        merged.merge(spec)
    end
    [merged, result]
end

#find_default_specialization(key) ⇒ Object?

Looks for objects registered in default_specialization under the given key, and returns the first one in the ancestor chain (i.e. the one tha thas been registered in the most specialized class)

Returns:

  • (Object, nil)

    the found object, or nil if none is registered under that key



12
# File 'lib/syskit/models/specialization_manager.rb', line 12

inherited_attribute(:default_specialization, :default_specializations, :map => true) { Hash.new }

#find_matching_specializations(selection) ⇒ [CompositionSpecialization,Array<CompositionSpecialization>]

Find the sets of specializations that match selection

Further disambiguation would, for instance, have to pick one of these sets and call

specialized_model(*candidates[selected_element])

to get the corresponding composition model

Returns:

  • ([CompositionSpecialization,Array<CompositionSpecialization>])

    set of (merged_specialization,atomic_specializations) pairs, in which:

    • merged_specialization is the Specialization instance representing the desired composite specialization

    • atomic_specializations is the set of single specializations that have been merged to obtain merged_specialization



560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
# File 'lib/syskit/models/specialization_manager.rb', line 560

def find_matching_specializations(selection)
    if specializations.empty? || selection.empty?
        return [[CompositionSpecialization.new, []]]
    end

    Models.debug do
        Models.debug "looking for specialization of #{composition_model.short_name} on"
        selection.each do |k, v|
            Models.debug "  #{k} => #{v}"
        end
        break
    end

    matching_specializations = each_specialization.find_all do |spec_model|
        spec_model.weak_match?(selection)
    end

    Models.debug do
        Models.debug "  #{matching_specializations.size} matching specializations found"
        matching_specializations.each do |m|
            Models.debug "    #{m.specialized_children}"
        end
        break
    end

    if matching_specializations.empty?
        return [[CompositionSpecialization.new, []]]
    end

    return partition_specializations(matching_specializations)
end

#has_default_specialization?(key) ⇒ Boolean

Returns true if an object is registered in default_specialization anywhere in the class hierarchy

Returns:

  • (Boolean)


12
# File 'lib/syskit/models/specialization_manager.rb', line 12

inherited_attribute(:default_specialization, :default_specializations, :map => true) { Hash.new }

#instanciate_all_possible_specializationsObject



319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
# File 'lib/syskit/models/specialization_manager.rb', line 319

def instanciate_all_possible_specializations
    all = partition_specializations(specializations.values)

    done_subsets = Set.new

    result = []
    all.each do |merged, set|
        (1..set.size).each do |subset_size|
            set.to_a.combination(subset_size) do |subset|
                subset = subset.to_set
                if !done_subsets.include?(subset)
                    merged = Specialization.new
                    subset.each { |spec| merged.merge(spec) }
                    result << specialized_model(merged, subset)
                    done_subsets << subset
                end
            end
        end
    end
    result
end

#instanciated_specializationsHash{Hash{String=>Model} => CompositionSpecialization}

Returns set of specialized composition models already instantiated with #specialized_model. The key is the specialization selectors and the value the composite specialization, in which CompositionSpecialization#composition_model returns the composition model

Returns:



347
348
349
350
351
352
353
# File 'lib/syskit/models/specialization_manager.rb', line 347

def instanciated_specializations
    root = composition_model.root_model
    if root == composition_model
        return (@instanciated_specializations ||= Hash.new)
    else return root.specializations.instanciated_specializations
    end
end

#matching_specialized_model(selection, options = Hash.new) ⇒ Model<Composition>

Looks for a single composition model that matches the given selection

Parameters:

  • selection (InstanceSelection)

    the current selection

  • options (Hash) (defaults to: Hash.new)

    a customizable set of options

Options Hash (options):

  • strict (Boolean) — default: true

    If true, an ambiguous match will make the method raise. Otherwise, the method will return the common subset of the matching specializations.

Returns:

Raises:



603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
# File 'lib/syskit/models/specialization_manager.rb', line 603

def matching_specialized_model(selection, options = Hash.new)
    options = Kernel.validate_options options,
        :strict => true,
        :specialization_hints => Set.new

    component_selection = selection.map_value do |_, selected|
        selected.selected.model.to_component_model
    end
    candidates = find_matching_specializations(component_selection)

    if candidates.size > 1
        filtered_candidates = candidates.find_all do |spec, _|
            options[:specialization_hints].any? do |hint|
                spec.weak_match?(hint)
            end
        end
        if !filtered_candidates.empty?
            candidates = filtered_candidates
        end
    end
    if candidates.size > 1
        filtered_candidates = candidates.find_all do |spec, _|
            spec.weak_match?(selection)
        end
        if !filtered_candidates.empty?
            candidates = filtered_candidates
        end
    end

    if candidates.empty?
        return composition_model
    elsif candidates.size > 1
        if options[:strict]
            selection = selection.map_value do |_, sel|
                sel.selected
            end
            raise AmbiguousSpecialization.new(composition_model, selection, candidates)
        else
            candidates = [find_common_specialization_subset(candidates)]
        end
    end

    specialized_model = specialized_model(*candidates.first)
    Models.debug do
        if specialized_model != composition_model
            Models.debug "using specialization #{specialized_model.short_name} of #{composition_model.short_name}"
        end
        break
    end
    return specialized_model
end

#normalize_specialization_mappings(mappings) ⇒ {String=>Model<Component>,Model<DataService>}

Transforms specialization specifications given to #specialize into a name => model_set mapping

Parameters:

  • mappings ({String,Model<DataService>,Model<Component>=>Model<DataService>,Model<Component>})

    specialization mappings. The mapping maps an object that selects a composition child, maps it to a model or set of models. The created specialization will be applied only when the selected child fullfills the models.

Returns:

Raises:

  • (ArgumentError)

    if a data service type is given as child specification and none of the children matches

  • (ArgumentError)

    if a data service type is given as child specification, but more than one child matches this service



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
# File 'lib/syskit/models/specialization_manager.rb', line 171

def normalize_specialization_mappings(mappings)
    # Normalize the user-provided specialization mapping
    new_spec = Hash.new
    mappings.each do |child, child_model|
        if Models.is_model?(child)
            children = composition_model.each_child.
                find_all { |name, child_definition| child_definition.fullfills?(child) }.
                map { |name, _| name }

            if children.empty?
                raise ArgumentError, "invalid specialization #{child.short_name} => #{child_model.short_name}: no child of #{composition_model.short_name} fullfills #{child.short_name}"
            elsif children.size > 1
                children = children.map do |child_name|
                    child_models = composition_model.find_child(child_name).
                        each_required_model.map(&:short_name).sort.join(",")
                    "#{child_name}: #{child_models}"
                end
                raise ArgumentError, "invalid specialization #{child.short_name} => #{child_model.short_name}: more than one child of #{composition_model.short_name} fullfills #{child.short_name} (#{children.sort.join("; ")}). You probably want to select one specifically by name"
            end
            child = children.first
        elsif !child.respond_to?(:to_str)
            raise ArgumentError, "invalid child selector #{child}"
        end

        child_model = Array(child_model)
        child_model.each do |m|
            if !Models.is_model?(m) || m.kind_of?(Models::BoundDataService)
                raise ArgumentError, "invalid specialization selector #{child} => #{child_model}: #{m} is not a component model or a data service model"
            end
        end

        new_spec[child.to_str] = child_model.to_set
    end
    return new_spec
end

#partition_specializations(specialization_set) ⇒ [(CompositionSpecialization,Array<CompositionSpecialization>)]

Partitions a set of specializations into the smallest number of partitions, where all the specializations in a subset of the partition can be applied together

Parameters:

Returns:

  • ([(CompositionSpecialization,Array<CompositionSpecialization>)])

    the partitioned subsets, where the second element of each pair is the set of specializations that can be applied together and the first element the CompositionSpecialization object that is created by merging all the specialization specifications from the second element



484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
# File 'lib/syskit/models/specialization_manager.rb', line 484

def partition_specializations(specialization_set)
    if specialization_set.empty?
        return []
    end

    # What we have to do here is:
    #
    #   for each S0 in specialization_set
    #     for each S1 in compatibilities(S0)
    #       there exists C in result which contain S0 and S1
    #     end
    #   end
    #
    # We gather all the capabilities and iteratively remove the ones
    # for which this property is fullfilled
    result = []
    specialization_set = specialization_set.to_set

    compatibilities = Hash.new
    specialization_set.each do |s0|
        compatibilities[s0] = (s0.compatibilities.to_set & specialization_set) << s0
    end
    compatibilities.each do |s0, remaining|
        # Iterate over the existing elements
        result.each do |merged, all|
            if all.include?(s0)
                remaining.subtract(all)
            elsif merged.compatible_with?(s0)
                # not there yet and compatible, add it
                merged.merge(s0)
                all << s0
                remaining.subtract(all)
            end
        end

        # Now, add new elements for what is left
        merged, all = nil
        while !remaining.empty?
            if !merged
                merged = CompositionSpecialization.new
                merged.merge(s0)
                all = [s0].to_set
            end

            remaining.delete(s1 = remaining.first)
            next if s0 == s1 # possible if the iteration on 'result' above did not find anything
            if merged.compatible_with?(s1)
                merged.merge(s1)
                all << s1
            else
                result << [merged, all]
                merged = nil
            end
        end
        if merged
            result << [merged, all]
        end
    end
    result
end

#register(specialization) ⇒ void

This method returns an undefined value.

Registers the given specialization on this manager

Parameters:



27
28
29
30
# File 'lib/syskit/models/specialization_manager.rb', line 27

def register(specialization)
    specialization.root_name = composition_model.root_model.name
    specializations[specialization.specialized_children] = specialization
end

#specializations{{String => Array<Model<DataService>,Model<Component>>}=>CompositionSpecialization}

The set of specializations defined on #composition_model

Returns:



21
# File 'lib/syskit/models/specialization_manager.rb', line 21

attribute(:specializations) { Hash.new }

#specialize(options = Hash.new, &block) ⇒ Object

Specifies a modification that should be applied on #composition_model when select children fullfill some specific models.

Parameters:

  • options (Hash) (defaults to: Hash.new)

    a customizable set of options

  • mappings ({String,Model<DataService>,Model<Component>=>Model<DataService>,Model<Component>})

    specialization mappings. The mapping maps an object that selects a composition child, maps it to a model or set of models. The created specialization will be applied only when the selected child fullfills the models.

Options Hash (options):

  • not (Boolean)

    If it is known that a specialization is in conflict with another, the :not option can be used. For instance, in the following code, only two specialization will exist: the one in which the Control child is a SimpleController and the one in which it is a FourWheelController.

    In the example below, if the :not option had not been used, three specializations would have been added: the same two than above, and the one case where 'Control' fullfills both the SimpleController and FourWheelController data services.

    @example

    specialize 'Control', SimpleController, :not => FourWheelController do
    end
    specialize 'Control', FourWheelController, :not => SimpleController do
    end
    

Raises:

  • (ArgumentError)

    if a data service type is given as child specification and none of the children matches

  • (ArgumentError)

    if a data service type is given as child specification, but more than one child matches this service



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
# File 'lib/syskit/models/specialization_manager.rb', line 88

def specialize(options = Hash.new, &block)
    Models.debug do
        Models.debug "trying to specialize #{composition_model.short_name}"
        Models.log_nest 2
        Models.debug "with"
        options.map do |name, models|
            Models.debug "  #{name} => #{models}"
        end

        Models.debug ""
        break
    end

    options, mappings = Kernel.filter_options options, :not => []
    if !options[:not].respond_to?(:to_ary)
        options[:not] = [options[:not]]
    end

    mappings = normalize_specialization_mappings(mappings)

    # validate the mappings
    validate_specialization_mappings(mappings)

    # register it
    if specialization = specializations[mappings]
        new_specialization = specialization.dup
    else
        new_specialization = CompositionSpecialization.new
    end
    # validate the block
    new_specialization.add(mappings, block)
    specialized_composition_model =
        create_specialized_model(new_specialization, [new_specialization])

    # ... and update compatibilities
    #
    # NOTE: this code does NOT updates compatibilities based on
    # whether two specialization selection models are compatible as,
    # by definition, the system should never try to instantiate one
    # of these (since the models that would trigger this
    # instantiation cannot be represented)
    each_specialization do |spec|
        next if spec == new_specialization || spec == new_specialization
        if compatible_specializations?(spec, new_specialization)
            spec.compatibilities << new_specialization
            new_specialization.compatibilities << spec
        else
            spec.compatibilities.delete(new_specialization)
            new_specialization.compatibilities.delete(spec)
        end
    end

    # and register the result
    if specialization
        deregister(specialization)
    end
    new_specialization.composition_model = specialized_composition_model
    register(new_specialization)

    # Finally, we create 
    new_specialization

ensure
    Models.debug do
        Models.log_nest -2
        break
    end
end

#specialized_model(composite_spec, applied_specializations = [composite_spec]) ⇒ Object

Returns the composition model that is a specialization of #composition_model, applying the set of specializations in 'applied_specializations' composite_spec is a Specialization object in which all the required specializations have been merged and applied_specializations the list of the specializations, separate.



361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
# File 'lib/syskit/models/specialization_manager.rb', line 361

def specialized_model(composite_spec, applied_specializations = [composite_spec])
    Models.debug do
        Models.debug "instanciating specializations: #{applied_specializations.map(&:to_s).sort.join(", ")}"
        Models.log_nest(2)
        break
    end

    if composite_spec.specialized_children.empty?
        return composition_model
    elsif current_model = instanciated_specializations[composite_spec.specialized_children]
        return current_model.composition_model
    end
    child_composition = create_specialized_model(composite_spec, applied_specializations)
    composite_spec.composition_model = child_composition
    instanciated_specializations[composite_spec.specialized_children] = composite_spec
    child_composition

ensure
    Models.debug do
        Models.log_nest -2
        break
    end
end

#validate_specialization_mappings(new_spec) ⇒ void

This method returns an undefined value.

Verifies that the child selection in new_spec is valid

Parameters:

  • mappings ({String,Model<DataService>,Model<Component>=>Model<DataService>,Model<Component>})

    specialization mappings. The mapping maps an object that selects a composition child, maps it to a model or set of models. The created specialization will be applied only when the selected child fullfills the models.

Raises:

  • (ArgumentError)

    if the spec refers to a child that does not exist

  • (ArgumentError)

    if the spec either selects on a model that is already provided by the corresponding child

  • (IncompatibleComponentModels)

    if the new model contains a component model that is incompatible with the current child's model



218
219
220
221
222
223
224
225
226
227
228
229
230
# File 'lib/syskit/models/specialization_manager.rb', line 218

def validate_specialization_mappings(new_spec)
    new_spec.each do |child_name, child_models|
        child_m = composition_model.find_child(child_name)
        if !child_m
            raise ArgumentError, "there is no child called #{child_name} in #{composition_model.short_name}"
        end

        merged = Placeholder.for(child_models).merge(child_m.model)
        if merged == child_m.model
            raise ArgumentError, "#{child_models.map(&:short_name).sort.join(",")} does not specify a specialization of #{child_m.model}"
        end
    end
end