Free cookie consent management tool by TermsFeed Policy Generator

source: trunk/sources/HeuristicLab.Optimization/3.3/MetaOptimizers/Experiment.cs @ 15632

Last change on this file since 15632 was 15583, checked in by swagner, 7 years ago

#2640: Updated year of copyrights in license headers

File size: 21.6 KB
RevLine 
[3267]1#region License Information
2/* HeuristicLab
[15583]3 * Copyright (C) 2002-2018 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
[3267]4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
[5419]23using System.Collections.Generic;
[3267]24using System.Drawing;
25using System.Linq;
[13000]26using System.Threading;
[15287]27using System.Threading.Tasks;
[3267]28using HeuristicLab.Collections;
29using HeuristicLab.Common;
30using HeuristicLab.Core;
31using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
32
33namespace HeuristicLab.Optimization {
34  /// <summary>
[5300]35  /// An experiment which contains multiple algorithms, batch runs or other experiments.
[3267]36  /// </summary>
[5300]37  [Item("Experiment", "An experiment which contains multiple algorithms, batch runs or other experiments.")]
[12504]38  [Creatable(CreatableAttribute.Categories.TestingAndAnalysis, Priority = 100)]
[3267]39  [StorableClass]
[4419]40  public sealed class Experiment : NamedItem, IOptimizer, IStorableContent {
41    public string Filename { get; set; }
42
[7201]43    public static new Image StaticItemImage {
44      get { return HeuristicLab.Common.Resources.VSImageLibrary.Event; }
45    }
[3267]46    public override Image ItemImage {
[3351]47      get {
[6534]48        if (ExecutionState == ExecutionState.Prepared) return HeuristicLab.Common.Resources.VSImageLibrary.ExperimentPrepared;
49        else if (ExecutionState == ExecutionState.Started) return HeuristicLab.Common.Resources.VSImageLibrary.ExperimentStarted;
50        else if (ExecutionState == ExecutionState.Paused) return HeuristicLab.Common.Resources.VSImageLibrary.ExperimentPaused;
51        else if (ExecutionState == ExecutionState.Stopped) return HeuristicLab.Common.Resources.VSImageLibrary.ExperimentStopped;
[7201]52        else return base.ItemImage;
[3351]53      }
[3267]54    }
55
56    [Storable]
57    private ExecutionState executionState;
58    public ExecutionState ExecutionState {
59      get { return executionState; }
60      private set {
61        if (executionState != value) {
62          executionState = value;
63          OnExecutionStateChanged();
[3351]64          OnItemImageChanged();
[3267]65        }
66      }
67    }
68
69    [Storable]
70    private TimeSpan executionTime;
71    public TimeSpan ExecutionTime {
72      get { return executionTime; }
73      private set {
74        executionTime = value;
75        OnExecutionTimeChanged();
76      }
77    }
78
[3280]79    [Storable]
[3274]80    private OptimizerList optimizers;
81    public OptimizerList Optimizers {
82      get { return optimizers; }
[3267]83    }
84
[3275]85    [Storable]
86    private RunCollection runs;
87    public RunCollection Runs {
88      get { return runs; }
[3716]89      private set {
90        if (value == null) throw new ArgumentNullException();
91        if (runs != value) {
92          if (runs != null) DeregisterRunsEvents();
93          runs = value;
94          if (runs != null) RegisterRunsEvents();
95        }
96      }
[3275]97    }
98
[15408]99    [Storable]
100    private int numberOfWorkers = 1;
101    public int NumberOfWorkers {
102      get { return numberOfWorkers; }
103      set {
104        if (value < 1) throw new ArgumentException("Number of Workers must not be lower than one.");
105        numberOfWorkers = value;
106      }
107    }
108
[5419]109    public IEnumerable<IOptimizer> NestedOptimizers {
110      get {
111        if (Optimizers == null) yield break;
112
113        foreach (IOptimizer opt in Optimizers) {
114          yield return opt;
115          foreach (IOptimizer nestedOpt in opt.NestedOptimizers)
116            yield return nestedOpt;
117        }
118      }
119    }
120
[6767]121    private bool experimentStarted = false;
122    private bool experimentStopped = false;
[15408]123
[15452]124    // track already started optimizers (.StartAsync does not set the executionstate immediately)
125    // and to avoid restarting optimizers that were manually paused/stopped by the user
126    private readonly IDictionary<IOptimizer, Task> startedOptimizers = new Dictionary<IOptimizer, Task>();
127    private IEnumerable<IOptimizer> StartableOptimizers {
128      get {
129        return Optimizers
130          .Where(x => x.ExecutionState == ExecutionState.Prepared || x.ExecutionState == ExecutionState.Paused)
131          .Where(o => !startedOptimizers.ContainsKey(o));  // all startable optimizers that were not startet yet
132      }
133    }
[3267]134
135    public Experiment()
136      : base() {
[3280]137      name = ItemName;
138      description = ItemDescription;
[3267]139      executionState = ExecutionState.Stopped;
140      executionTime = TimeSpan.Zero;
[3280]141      optimizers = new OptimizerList();
[8975]142      Runs = new RunCollection { OptimizerName = Name };
[3280]143      Initialize();
[3267]144    }
[3280]145    public Experiment(string name)
146      : base(name) {
147      description = ItemDescription;
[3267]148      executionState = ExecutionState.Stopped;
149      executionTime = TimeSpan.Zero;
[3280]150      optimizers = new OptimizerList();
[8975]151      Runs = new RunCollection { OptimizerName = Name };
[3280]152      Initialize();
[3267]153    }
[3280]154    public Experiment(string name, string description)
155      : base(name, description) {
[3267]156      executionState = ExecutionState.Stopped;
157      executionTime = TimeSpan.Zero;
[3280]158      optimizers = new OptimizerList();
[8975]159      Runs = new RunCollection { OptimizerName = Name };
[3280]160      Initialize();
[3267]161    }
[3280]162    [StorableConstructor]
[6767]163    private Experiment(bool deserializing) : base(deserializing) { }
[4722]164    [StorableHook(HookType.AfterDeserialization)]
165    private void AfterDeserialization() {
166      Initialize();
167    }
168    private Experiment(Experiment original, Cloner cloner)
169      : base(original, cloner) {
170      executionState = original.executionState;
171      executionTime = original.executionTime;
172      optimizers = cloner.Clone(original.optimizers);
173      runs = cloner.Clone(original.runs);
[6767]174
175      experimentStarted = original.experimentStarted;
176      experimentStopped = original.experimentStopped;
[15408]177      numberOfWorkers = original.numberOfWorkers;
[4722]178      Initialize();
179    }
180    public override IDeepCloneable Clone(Cloner cloner) {
181      if (ExecutionState == ExecutionState.Started) throw new InvalidOperationException(string.Format("Clone not allowed in execution state \"{0}\".", ExecutionState));
182      return new Experiment(this, cloner);
183    }
[3267]184
[3280]185    private void Initialize() {
186      RegisterOptimizersEvents();
187      foreach (IOptimizer optimizer in optimizers)
188        RegisterOptimizerEvents(optimizer);
[3716]189      if (runs != null) RegisterRunsEvents();
[3280]190    }
191
[3267]192    public void Prepare() {
[3275]193      Prepare(false);
[3267]194    }
[3275]195    public void Prepare(bool clearRuns) {
[3267]196      if ((ExecutionState != ExecutionState.Prepared) && (ExecutionState != ExecutionState.Paused) && (ExecutionState != ExecutionState.Stopped))
197        throw new InvalidOperationException(string.Format("Prepare not allowed in execution state \"{0}\".", ExecutionState));
[6767]198      if (Optimizers.Count == 0) return;
199
200      if (clearRuns) runs.Clear();
[7241]201
202      experimentStarted = false;
203      experimentStopped = false;
[8194]204      foreach (IOptimizer optimizer in Optimizers.Where(x => x.ExecutionState != ExecutionState.Started)) {
205        // a race-condition may occur when the optimizer has changed the state by itself in the meantime
[15287]206        try { optimizer.Prepare(clearRuns); } catch (InvalidOperationException) { }
[8194]207      }
[3267]208    }
209    public void Start() {
[15287]210      Start(CancellationToken.None);
211    }
212    public void Start(CancellationToken cancellationToken) {
[3267]213      if ((ExecutionState != ExecutionState.Prepared) && (ExecutionState != ExecutionState.Paused))
214        throw new InvalidOperationException(string.Format("Start not allowed in execution state \"{0}\".", ExecutionState));
[6767]215
[15452]216      startedOptimizers.Clear();
217      if (!StartableOptimizers.Any()) return;
[15408]218
[6767]219      experimentStarted = true;
220      experimentStopped = false;
[15408]221
222      using (var availableWorkers = new SemaphoreSlim(NumberOfWorkers, NumberOfWorkers)) {
[15452]223        while (StartableOptimizers.Any()) {
[15408]224          try {
225            availableWorkers.Wait(cancellationToken);
[15452]226            var optimizer = StartableOptimizers.FirstOrDefault();
227            if (experimentStopped || !experimentStarted || optimizer == null) break;
228
229            var startedTask = optimizer.StartAsync(cancellationToken).ContinueWith(async t => {
230              availableWorkers.Release(); // is guaranteed to be not disposed yet because Task.WaitAll blocks before the end of the using
231              await t; // trigger a potential exception on the optimizerTask
232            });
233            startedOptimizers.Add(optimizer, startedTask.Unwrap()); // unwrap task because lambda of .ContinueWith is async
[15408]234          } catch (InvalidOperationException) { } catch (OperationCanceledException) { }
235        }
236
[15560]237        Task.WaitAll(startedOptimizers.Values.ToArray()); // retrieve exeptions of the asyncrounously started optimizer
[8194]238      }
[3267]239    }
[15287]240    public async Task StartAsync() { await StartAsync(CancellationToken.None); }
241    public async Task StartAsync(CancellationToken cancellationToken) {
242      await AsyncHelper.DoAsync(Start, cancellationToken);
243    }
[3267]244    public void Pause() {
245      if (ExecutionState != ExecutionState.Started)
246        throw new InvalidOperationException(string.Format("Pause not allowed in execution state \"{0}\".", ExecutionState));
[6767]247      if (Optimizers.Count == 0) return;
248
249      experimentStarted = false;
250      experimentStopped = false;
[8194]251      foreach (IOptimizer optimizer in Optimizers.Where(x => x.ExecutionState == ExecutionState.Started)) {
252        // a race-condition may occur when the optimizer has changed the state by itself in the meantime
[15408]253        try { optimizer.Pause(); } catch (InvalidOperationException) { } catch (NotSupportedException) { }
[8194]254      }
[3267]255    }
256    public void Stop() {
257      if ((ExecutionState != ExecutionState.Started) && (ExecutionState != ExecutionState.Paused))
258        throw new InvalidOperationException(string.Format("Stop not allowed in execution state \"{0}\".", ExecutionState));
[6767]259      if (Optimizers.Count == 0) return;
260
261      experimentStarted = false;
262      experimentStopped = true;
[7241]263      if (Optimizers.Any(x => (x.ExecutionState == ExecutionState.Started) || (x.ExecutionState == ExecutionState.Paused))) {
[8194]264        foreach (var optimizer in Optimizers.Where(x => (x.ExecutionState == ExecutionState.Started) || (x.ExecutionState == ExecutionState.Paused))) {
265          // a race-condition may occur when the optimizer has changed the state by itself in the meantime
[15287]266          try { optimizer.Stop(); } catch (InvalidOperationException) { }
[8194]267        }
[7241]268      } else {
269        OnStopped();
270      }
[3267]271    }
272
273    #region Events
[8738]274    protected override void OnNameChanged() {
275      base.OnNameChanged();
[8975]276      Runs.OptimizerName = Name;
[8738]277    }
278
[3267]279    public event EventHandler ExecutionStateChanged;
280    private void OnExecutionStateChanged() {
281      EventHandler handler = ExecutionStateChanged;
282      if (handler != null) handler(this, EventArgs.Empty);
283    }
284    public event EventHandler ExecutionTimeChanged;
285    private void OnExecutionTimeChanged() {
286      EventHandler handler = ExecutionTimeChanged;
287      if (handler != null) handler(this, EventArgs.Empty);
288    }
289    public event EventHandler Prepared;
290    private void OnPrepared() {
[15560]291      if (ExecutionState == ExecutionState.Prepared) return;
[3267]292      ExecutionState = ExecutionState.Prepared;
293      EventHandler handler = Prepared;
294      if (handler != null) handler(this, EventArgs.Empty);
295    }
296    public event EventHandler Started;
297    private void OnStarted() {
[15560]298      if (ExecutionState == ExecutionState.Started) return;
[3267]299      ExecutionState = ExecutionState.Started;
300      EventHandler handler = Started;
301      if (handler != null) handler(this, EventArgs.Empty);
302    }
303    public event EventHandler Paused;
304    private void OnPaused() {
[15560]305      if (ExecutionState == ExecutionState.Paused) return;
[3267]306      ExecutionState = ExecutionState.Paused;
307      EventHandler handler = Paused;
308      if (handler != null) handler(this, EventArgs.Empty);
309    }
310    public event EventHandler Stopped;
311    private void OnStopped() {
[15560]312      if (ExecutionState == ExecutionState.Stopped) return;
[3267]313      ExecutionState = ExecutionState.Stopped;
314      EventHandler handler = Stopped;
315      if (handler != null) handler(this, EventArgs.Empty);
316    }
317    public event EventHandler<EventArgs<Exception>> ExceptionOccurred;
318    private void OnExceptionOccurred(Exception exception) {
319      EventHandler<EventArgs<Exception>> handler = ExceptionOccurred;
320      if (handler != null) handler(this, new EventArgs<Exception>(exception));
321    }
322
[3274]323    private void RegisterOptimizersEvents() {
324      Optimizers.CollectionReset += new CollectionItemsChangedEventHandler<IndexedItem<IOptimizer>>(Optimizers_CollectionReset);
325      Optimizers.ItemsAdded += new CollectionItemsChangedEventHandler<IndexedItem<IOptimizer>>(Optimizers_ItemsAdded);
326      Optimizers.ItemsRemoved += new CollectionItemsChangedEventHandler<IndexedItem<IOptimizer>>(Optimizers_ItemsRemoved);
327      Optimizers.ItemsReplaced += new CollectionItemsChangedEventHandler<IndexedItem<IOptimizer>>(Optimizers_ItemsReplaced);
[3267]328    }
[3274]329    private void DeregisterOptimizersEvents() {
330      Optimizers.CollectionReset -= new CollectionItemsChangedEventHandler<IndexedItem<IOptimizer>>(Optimizers_CollectionReset);
331      Optimizers.ItemsAdded -= new CollectionItemsChangedEventHandler<IndexedItem<IOptimizer>>(Optimizers_ItemsAdded);
332      Optimizers.ItemsRemoved -= new CollectionItemsChangedEventHandler<IndexedItem<IOptimizer>>(Optimizers_ItemsRemoved);
333      Optimizers.ItemsReplaced -= new CollectionItemsChangedEventHandler<IndexedItem<IOptimizer>>(Optimizers_ItemsReplaced);
[3267]334    }
[3274]335    private void Optimizers_CollectionReset(object sender, CollectionItemsChangedEventArgs<IndexedItem<IOptimizer>> e) {
[4115]336      foreach (IndexedItem<IOptimizer> item in e.OldItems)
337        RemoveOptimizer(item.Value);
[4110]338      foreach (IndexedItem<IOptimizer> item in e.Items)
339        AddOptimizer(item.Value);
[3267]340    }
[3274]341    private void Optimizers_ItemsAdded(object sender, CollectionItemsChangedEventArgs<IndexedItem<IOptimizer>> e) {
[4110]342      foreach (IndexedItem<IOptimizer> item in e.Items)
343        AddOptimizer(item.Value);
[3267]344    }
[3274]345    private void Optimizers_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<IndexedItem<IOptimizer>> e) {
[4115]346      foreach (IndexedItem<IOptimizer> item in e.Items)
347        RemoveOptimizer(item.Value);
[3267]348    }
[3274]349    private void Optimizers_ItemsReplaced(object sender, CollectionItemsChangedEventArgs<IndexedItem<IOptimizer>> e) {
[4115]350      foreach (IndexedItem<IOptimizer> item in e.OldItems)
351        RemoveOptimizer(item.Value);
[4110]352      foreach (IndexedItem<IOptimizer> item in e.Items)
353        AddOptimizer(item.Value);
[3267]354    }
[4110]355    private void AddOptimizer(IOptimizer optimizer) {
356      RegisterOptimizerEvents(optimizer);
357      Runs.AddRange(optimizer.Runs);
358      optimizer.Prepare();
[4551]359      if (ExecutionState == ExecutionState.Stopped && optimizer.ExecutionState == ExecutionState.Prepared)
360        OnPrepared();
[4110]361    }
[4115]362    private void RemoveOptimizer(IOptimizer optimizer) {
363      DeregisterOptimizerEvents(optimizer);
364      Runs.RemoveRange(optimizer.Runs);
[4551]365      if (ExecutionState == ExecutionState.Prepared && !optimizers.Any(opt => opt.ExecutionState == ExecutionState.Prepared))
366        OnStopped();
[4115]367    }
[3267]368
[3274]369    private void RegisterOptimizerEvents(IOptimizer optimizer) {
370      optimizer.ExceptionOccurred += new EventHandler<EventArgs<Exception>>(optimizer_ExceptionOccurred);
371      optimizer.ExecutionTimeChanged += new EventHandler(optimizer_ExecutionTimeChanged);
372      optimizer.Paused += new EventHandler(optimizer_Paused);
373      optimizer.Prepared += new EventHandler(optimizer_Prepared);
374      optimizer.Started += new EventHandler(optimizer_Started);
375      optimizer.Stopped += new EventHandler(optimizer_Stopped);
[3280]376      optimizer.Runs.CollectionReset += new CollectionItemsChangedEventHandler<IRun>(optimizer_Runs_CollectionReset);
377      optimizer.Runs.ItemsAdded += new CollectionItemsChangedEventHandler<IRun>(optimizer_Runs_ItemsAdded);
378      optimizer.Runs.ItemsRemoved += new CollectionItemsChangedEventHandler<IRun>(optimizer_Runs_ItemsRemoved);
[3267]379    }
[3274]380    private void DeregisterOptimizerEvents(IOptimizer optimizer) {
381      optimizer.ExceptionOccurred -= new EventHandler<EventArgs<Exception>>(optimizer_ExceptionOccurred);
382      optimizer.ExecutionTimeChanged -= new EventHandler(optimizer_ExecutionTimeChanged);
383      optimizer.Paused -= new EventHandler(optimizer_Paused);
384      optimizer.Prepared -= new EventHandler(optimizer_Prepared);
385      optimizer.Started -= new EventHandler(optimizer_Started);
386      optimizer.Stopped -= new EventHandler(optimizer_Stopped);
[3280]387      optimizer.Runs.CollectionReset -= new CollectionItemsChangedEventHandler<IRun>(optimizer_Runs_CollectionReset);
388      optimizer.Runs.ItemsAdded -= new CollectionItemsChangedEventHandler<IRun>(optimizer_Runs_ItemsAdded);
389      optimizer.Runs.ItemsRemoved -= new CollectionItemsChangedEventHandler<IRun>(optimizer_Runs_ItemsRemoved);
[3267]390    }
[8170]391
392    private readonly object locker = new object();
[13000]393    private readonly object runsLocker = new object();
[3274]394    private void optimizer_ExceptionOccurred(object sender, EventArgs<Exception> e) {
[8170]395      lock (locker)
396        OnExceptionOccurred(e.Value);
[3267]397    }
[3274]398    private void optimizer_ExecutionTimeChanged(object sender, EventArgs e) {
[13000]399      // only wait for maximally 100ms to acquire lock, otherwise return and don't update the execution time
400      var success = Monitor.TryEnter(locker, 100);
401      if (!success) return;
402      try {
[8170]403        ExecutionTime = Optimizers.Aggregate(TimeSpan.Zero, (t, o) => t + o.ExecutionTime);
[15287]404      } finally {
[13000]405        Monitor.Exit(locker);
406      }
[3274]407    }
408    private void optimizer_Paused(object sender, EventArgs e) {
[15452]409      UpdateExecutionState();
[3267]410    }
[3274]411    private void optimizer_Prepared(object sender, EventArgs e) {
[15452]412      UpdateExecutionState();
[3267]413    }
[3274]414    private void optimizer_Started(object sender, EventArgs e) {
[8170]415      lock (locker)
416        if (ExecutionState != ExecutionState.Started) OnStarted();
[3267]417    }
[3274]418    private void optimizer_Stopped(object sender, EventArgs e) {
[15452]419      UpdateExecutionState();
420    }
421    private void UpdateExecutionState() {
422      // Execution states of the Experiment are determined using the following _basic_ rules:
423      //   if any Optimizer is Started      => Experiment is Started  (2. if)
424      //   if any Optimizer is Paused       => Experiment is Paused   (3. if)
425      //   if any Optimizer is Prepared     => Experiment is Prepared (5. if)
426      //   else (all Optimizer are Stopped) => Experiment is Stopped  (6. if)
427      // Additional there are two extra rules:
428      //   if the Experiment is running and there are still optimizers that can be started => keep the Experiment Running (1. if)
429      //   if experiment-stop is pending: Stop Experiment even if there are still Prepared Optimizer               (4. if)
430
[8129]431      lock (locker) {
[15452]432        // 1. experiment is running & further startable optimizers are available => continue executing
[15560]433        if (experimentStarted && StartableOptimizers.Any())
434          return;
[15452]435
436        // 2. any optimizer is running => continue executing
[15560]437        if (Optimizers.Any(x => x.ExecutionState == ExecutionState.Started))
[15452]438          return;
439
[15560]440        experimentStarted = false;
[15452]441        // 3. any optimizer is paused => experiment paused
[15560]442        if (Optimizers.Any(x => x.ExecutionState == ExecutionState.Paused))
[15452]443          OnPaused();
444
445        // 4. stop pending & all optimizers either stopped or prepared => experiment stopped
446        else if (experimentStopped)
447          OnStopped();
448
449        // 5. any optimizer prepared => experiment prepared
450        else if (Optimizers.Any(x => x.ExecutionState == ExecutionState.Prepared))
451          OnPrepared();
452
453        // 6. (else) all optimizers stopped
454        else
455          OnStopped();
[3267]456      }
457    }
[15452]458
[3280]459    private void optimizer_Runs_CollectionReset(object sender, CollectionItemsChangedEventArgs<IRun> e) {
[13000]460      lock (runsLocker) {
[8170]461        Runs.RemoveRange(e.OldItems);
462        Runs.AddRange(e.Items);
463      }
[3275]464    }
[3280]465    private void optimizer_Runs_ItemsAdded(object sender, CollectionItemsChangedEventArgs<IRun> e) {
[13000]466      lock (runsLocker)
[8170]467        Runs.AddRange(e.Items);
[3275]468    }
[3280]469    private void optimizer_Runs_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<IRun> e) {
[13000]470      lock (runsLocker)
[8170]471        Runs.RemoveRange(e.Items);
[3275]472    }
[3716]473
474    private void RegisterRunsEvents() {
475      runs.CollectionReset += new CollectionItemsChangedEventHandler<IRun>(Runs_CollectionReset);
476      runs.ItemsRemoved += new CollectionItemsChangedEventHandler<IRun>(Runs_ItemsRemoved);
477    }
478    private void DeregisterRunsEvents() {
479      runs.CollectionReset -= new CollectionItemsChangedEventHandler<IRun>(Runs_CollectionReset);
480      runs.ItemsRemoved -= new CollectionItemsChangedEventHandler<IRun>(Runs_ItemsRemoved);
481    }
482    private void Runs_CollectionReset(object sender, CollectionItemsChangedEventArgs<IRun> e) {
483      foreach (IOptimizer optimizer in Optimizers)
484        optimizer.Runs.RemoveRange(e.OldItems);
485    }
486    private void Runs_ItemsRemoved(object sender, CollectionItemsChangedEventArgs<IRun> e) {
487      foreach (IOptimizer optimizer in Optimizers)
488        optimizer.Runs.RemoveRange(e.Items);
489    }
[3267]490    #endregion
491  }
492}
Note: See TracBrowser for help on using the repository browser.