2

I am trying to write a method that takes an IConnectableObservable, does some processing on it and returns a new IConnectableObservable that streams the processed data plus some additional items. The sequence being streamed is finite, but it has side effects so it needs to be run only once. However, I am trying to do two things with it:

  1. Transform each element in the stream using a Select query.
  2. Collect each element in the stream into an array and do some processing on the array and stream the results.

Below is my best attempt at doing this, but I feel that there probably is a superior way of doing this that I haven't figured out.

protected override IConnectableObservable<ITestResult<ITestOutput<double, double>, ITestLimit<double>>> ProcessOutput(
    IConnectableObservable<ITestOutput<double, double>> output, InputVerificationTestCase testCase)
{
    var obsResults = output.Select(o =>
    {
        var limits = GenerateDcAccuracyLimits.CalculateAbsoluteLimits(o.Input, testCase.FullScaleRange, testCase.Limits);
        return new TestResult<ITestOutput<double, double>, ITestLimit<double>>
        {
            Component = "Variable Gain Module Input",
            Description = "Measurement Accuracy",
            Limits = limits,
            Output = o,
            Passed = _validationService.Validate(o.Result, limits)
        };
    });

    var observable = Observable.Create<ITestResult<ITestOutput<double, double>, ITestLimit<double>>>(obs =>
    {
        var resultTask = obsResults.ForEachAsync(obs.OnNext);
        var fitTask = output.ToArray().ForEachAsync(arr =>
        {
            resultTask.Wait();
            var fit = ComputeErrorFit(arr, testCase);
            obs.OnNext(GetGainErrorResult(fit.Item2, testCase));
        });
        output.Connect();
        Task.WaitAll(resultTask, fitTask);
        obs.OnCompleted();
        return Disposable.Empty;
    });

    return observable.Publish();
}

Edited 10/7/2015:

Here is the rest of the code:

private ITestResult<ITestOutput<double, double>, ITestLimit<double>> GetGainErrorResult(double gainError, InputVerificationTestCase testCase)
{
    var gainErrorLimit = GenerateDcAccuracyLimits.CalculateGainErrorLimits(testCase.FullScaleRange, testCase.Limits);
    return new TestResult<ITestOutput<double, double>, ITestLimit<double>>
    {
        Component = "Variable Gain Module Input",
        Description = "Gain Error",
        Passed = _validationService.Validate(gainError, gainErrorLimit),
        Output = new TestOutput<double, double> { Input = 0, Result = gainError },
        Limits = gainErrorLimit
    };
}

private Tuple<double, double> ComputeErrorFit(ITestOutput<double, double>[] outputs, InputChannelTestCase testCase)
{
    var input = outputs.Select(o => o.Input);
    var vErr = outputs.Select(o => o.Result - o.Input);
    return Fit.Line(input.ToArray(), vErr.ToArray());
}

Also in an abstract base class, I have the following:

public IConnectableObservable<TOutput> RunSingleChannel(TCase testCase)
{
    dut.Acquisition.SampleRate.Value = SampleRate;
    dut.AnalogChannels[testCase.Channel].InputRange.Value = testCase.FullScaleRange;
    var testOutput = CreateTestProcedure(testCase.Channel).RunAsync(testCase.InputVoltages);
    return ProcessOutput(testOutput.Replay(), testCase);
}

protected abstract IConnectableObservable<TOutput> ProcessOutput(IConnectableObservable<ITestOutput<double, TAcq>> output, TCase testCase);
  • Just as a quick bit of feedback - whenever you do `return Disposable.Empty;` you are doing something wrong. – Enigmativity Oct 07 '15 at 07:00
  • Next, mixing "monads" is generally bad. So all of the Observable and Task code that you've got mixed isn't the right way to go. – Enigmativity Oct 07 '15 at 07:03
  • And, you've not given us enough code to compile the `ProcessOutput` method. We then have a hard time trying to refactor the code. Could you please post a [Minimal, Complete, and Verifiable](https://stackoverflow.com/help/mcve) code sample? – Enigmativity Oct 07 '15 at 07:05

1 Answers1

3

It seems that you're going about doing things the hard way with Rx. You really need to avoid mixing in Tasks with Observables. They make your code hard to reason about and often lead to deadlocks and other concurrency issues.

You should try something like this instead:

protected override IConnectableObservable<ITestResult<ITestOutput<double, double>, ITestLimit<double>>> ProcessOutput(
    IConnectableObservable<ITestOutput<double, double>> output, InputVerificationTestCase testCase)
{
    var source = output.RefCount();
    return
        source
            .Select(o =>
            {
                var limits = GenerateDcAccuracyLimits.CalculateAbsoluteLimits(o.Input, testCase.FullScaleRange, testCase.Limits);
                return new TestResult<ITestOutput<double, double>, ITestLimit<double>>
                {
                    Component = "Variable Gain Module Input",
                    Description = "Measurement Accuracy",
                    Limits = limits,
                    Output = o,
                    Passed = _validationService.Validate(o.Result, limits)
                };
            })
            .Merge(
                source
                    .ToArray()
                    .Select(arr => GetGainErrorResult(ComputeErrorFit(arr, testCase).Item2, testCase)))
            .Publish();
}

It's a little odd that you're using connectable observables, but the above should roughly be doing what you need.

I've tested the code using this sample:

public IConnectableObservable<int> ProcessOutput(IConnectableObservable<int> output)
{
    var source = output.RefCount();
    return
        source
            .Merge(source.ToArray().Select(arr => arr.Sum()))
            .Publish();
}

void Main()
{
    var output = Observable.Range(1, 10).Publish();

    var processed = ProcessOutput(output);

    processed.Subscribe(x => Console.WriteLine(x));

    processed.Connect();
}

Which outputs:

1
2
3
4
5
6
7
8
9
10
55

I've also checked that the original observable values are only produced once.

Enigmativity
  • 113,464
  • 11
  • 89
  • 172
  • I arrived at same result (with `Concat` instead of `Merge`, to guarantee ordering), just wondering if one'd need to use `Replay` instead of `Refcount` to further guarantee the behavior. Otherwise it may be that the first subscription triggers all stream events before the ToArray one kicks in ? – Gluck Oct 07 '15 at 07:46
  • Gah! You beat me too it, I knew I spent too much time formatting. =) – paulpdaniels Oct 07 '15 at 07:49
  • @Gluck - I don't think `.Concat` would work because of the hotness of `source` so the `.ToArray()` would return nothing because the subscription would only happen when the sequence has already ended (or possibly it would cause a second fresh subscription to the `output` observable.) – Enigmativity Oct 07 '15 at 08:43
  • Yes, hence the Replay. but I'm wondering if the Merge version can't get into the same issue. – Gluck Oct 07 '15 at 08:49
  • @Gluck - `Merge` shouldn't because of the `ToArray`. – Enigmativity Oct 07 '15 at 11:29
  • only potential problem with using `Merge` is the OP doesn't call `ComputeErrorFit` until after all of the test cases have run, and this value is the final value emitted (see `resultTask.Wait()`. Might need to use `Concat` and wrap the construct in a `.Publish(Func)` overload to handle the hotness issue. In some ways this is really just a `Scan` operation. – Brandon Oct 07 '15 at 13:35
  • @Brandon - I was thinking about using `Scan` but there's an extra value being produced. I then thought about using `Zip` with a `Skip(1)` to add the extra element before the `Scan` but that seemed clunky. – Enigmativity Oct 07 '15 at 23:18