Skip to content

Conversation

aditya-vk
Copy link
Contributor

@aditya-vk aditya-vk commented May 5, 2018

This PR adds a Sequential Sampleable. It samples in the order given, moving to the next one upon exhaustion of the current sampleable. This is useful, for instance, in solving IK, in sampling the current configuration first before other random/informed sampling.


Before creating a pull request

  • Document new methods and classes
  • Format code with make format

Before merging a pull request

  • Set version target by selecting a milestone on the right side
  • Summarize this change in CHANGELOG.md
  • Add unit test(s) for this change

@codecov
Copy link

codecov bot commented May 5, 2018

Codecov Report

Merging #393 into master will decrease coverage by 0.01%.
The diff coverage is 78.26%.

@@            Coverage Diff             @@
##           master     #393      +/-   ##
==========================================
- Coverage   80.96%   80.94%   -0.02%     
==========================================
  Files         201      203       +2     
  Lines        5705     5774      +69     
==========================================
+ Hits         4619     4674      +55     
- Misses       1086     1100      +14
Impacted Files Coverage Δ
include/aikido/constraint/SequentialSampleable.hpp 100% <100%> (ø)
src/constraint/CartesianProductSampleable.cpp 78.33% <40%> (-3.49%) ⬇️
src/constraint/SequentialSampleable.cpp 80.95% <80.95%> (ø)
src/constraint/FiniteSampleable.cpp 86.88% <0%> (+1.63%) ⬆️

#ifndef AIKIDO_CONSTRAINT_SEQUENTIALSAMPLEABLE_HPP_
#define AIKIDO_CONSTRAINT_SEQUENTIALSAMPLEABLE_HPP_

#include "Sampleable.hpp"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Use full path (e.g., "aikido/constraint/Sampleable.hpp")


private:
statespace::StateSpacePtr mStateSpace;
std::vector<SampleablePtr> mSampleables;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add docstring

@@ -0,0 +1,117 @@
#include <aikido/constraint/SequentialSampleable.hpp>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: #include "aikido/constraint/SequentialSampleable.hpp"

namespace constraint {

// TODO (avk): Should I declare the class first and then define?
// What is "good" C++ convention? Don't tell me it's a preference JS! -.-
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's necessary when defining in a source file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, shall we add docstring during definitions, which we generally do not do right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shall we add docstring during definitions

I think so. Although they are not exposed to the users, it would be very helpful for us (i.e., developers).

/// \param[in] sampleables Set of sampleables.
SequentialSampleable(
statespace::StateSpacePtr stateSpace,
std::vector<SampleablePtr> sampleables);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pass sampleables by const reference to avoid the unnecessary copy. Looking at the implementation, you used std::move(...). (a) Passing by const reference and (b) passing by value + std::move() are technically the same, but we only use (b) for smart pointers to indicate the ownership is being shared or transferred.

{
// Do nothing
// TODO (avk): Should we again check equivalence of statespaces between
// that of each generator and mStateSpace? I don't think it's necessary.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this class is vulnerable in the case of empty generators.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getNumSamples() returns 0 after checking for empty vector.


int getNumSamples() const override
{
return mGenerators[mIndex]->getNumSamples();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if this logic is following the description of getNumSamples():

Gets an upper bound on the number of samples remaining or NO_LIMIT.

if (mIndex >= mGenerators.size())
return false;

return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function should return true when:

  1. the number of remaining generators is not zero and
  2. one of the remaining generators is able to sample.

It seems the current implementation is missing 2.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current implementation with the iterating through generators until sampling is possible, (2) is implicitly taken care of

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds like canSample() works correctly only when it's used in sample(), which I don't think that's a good idea. Also, this doesn't agree with the docstrion of canSample():

/// Returns whether getNumSamples() > 0.

I propose changing this function to return getNumSamples() > 0; and changing canSample() not to call this function and to check the size of mGenerators directly in canSample().

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible for one of the remaining generators to not be sampleable? Then even though you have generators remaining (this implementation would return true), it still seems like you wouldn't be sample (logically, maybe it should return false). What if we just do something like

for (std::size_t i = mIndex; i < mGenerators.size(); ++i)
  if (mGenerators[i].canSample())
    return true;
return false;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brianhou 's version would be more efficient because returns getNumSamples() > 0 can be more expensive as it computes the exact number of samples while @brianhou 's version can early terminate.

private:
statespace::StateSpacePtr mStateSpace;
std::vector<std::unique_ptr<SampleGenerator>> mGenerators;
std::size_t mIndex;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add docstrings.

bool sample(statespace::StateSpace::State* state)
{
if (!mGenerators[mIndex]->canSample())
mIndex++;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this function should keep iterating until it finds a sampleable generator instead of just check only one generator.

@aditya-vk aditya-vk added the pr: work-in-progress Calls for preliminary review of the PR. Remove when PR is ready for complete review. label May 6, 2018
@aditya-vk aditya-vk removed the pr: work-in-progress Calls for preliminary review of the PR. Remove when PR is ready for complete review. label May 6, 2018
/// \param[in] sampleables Set of sampleables.
SequentialSampleable(
statespace::StateSpacePtr stateSpace,
std::vector<SampleablePtr>& sampleables);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant passing by const reference rather than just reference. 😄

private:
/// StateSpace in which the constraint operates.
statespace::StateSpacePtr mStateSpace;
/// Set of sampleables.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Add empty line between members.

public:
SequentialSampleGenerator(
statespace::StateSpacePtr stateSpace,
std::vector<std::unique_ptr<SampleGenerator>>& generators)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should pass by value for generators (and move) because it's transferring the onwerships.

, mIndex(0)
{
for (std::size_t i = 0; i < mGenerators.size(); ++i)
assert(mGenerators[i]->getStateSpace() == mStateSpace);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please note that assert is ignored in release mode. This code will not be built in release mode since the for-statement is empty.

We could either:

#ifndef NDEBUG // debug mode
  for (const auto& generator : mGenerators)
    assert(generator->getStateSpace() == mStateSpace);
#endif

or

  for (const auto& generator : mGenerators)
  {
    assert(generator->getStateSpace() == mStateSpace);
  }

I slightly prefer former version. 😄

if (mIndex >= mGenerators.size())
return false;

return true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It sounds like canSample() works correctly only when it's used in sample(), which I don't think that's a good idea. Also, this doesn't agree with the docstrion of canSample():

/// Returns whether getNumSamples() > 0.

I propose changing this function to return getNumSamples() > 0; and changing canSample() not to call this function and to check the size of mGenerators directly in canSample().


mIndex++;
}
return false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic (i.e., "try the current generator to sample and return the result") is possible to return false even when one of the remaining generators can sample, which I don't think that's the expected behavior of sample.

I propose changing this logic to something like:

index2 = index
for generator in generators[index2:]:
  result = generator.sample(state);
  index += 1
  if result:
    return True
return False

so that it tries to sample until it can sample and returns false when all the remaining generators are not able to draw a sample.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. Got it.
Your proposal will account for sample() may transiently fail, i.e. return false, at any point before then

if (mIndex >= mGenerators.size())
return false;

return true;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible for one of the remaining generators to not be sampleable? Then even though you have generators remaining (this implementation would return true), it still seems like you wouldn't be sample (logically, maybe it should return false). What if we just do something like

for (std::size_t i = mIndex; i < mGenerators.size(); ++i)
  if (mGenerators[i].canSample())
    return true;
return false;

// Documentation inherited
int getNumSamples() const override
{
if (mGenerators.empty())
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this might be unnecessary. If this happens, the for loop wouldn't execute and numSamples would be 0 anyway.

, mGenerators(std::move(generators))
, mIndex(0)
{
#ifndef NDEBUG
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jslee02 what's the rule with indentation and preprocessor commands? Should this be indented or not? Should the for loop be indented more?

Copy link
Member

@jslee02 jslee02 May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We haven't explicitly decided yet, but most people don't indent due to preprocessors. So my preference is like:

  {
#ifndef NDEBUG // debug mode
    for (...)
      assert(...);
#endif
  }

or

  {
    #ifndef NDEBUG // debug mode
    for (...)
      assert(...);
    #endif
  }

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like our current settings for formatting prefer the former.

return true;
}
mIndex++;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This logic is still inefficient because of calling canSample(); canSample() iterates over the generators remaining while canSample() is called repeatidly in this logic.

It seems that calling both of canSample() is not necessary. What would be the problem if we remove calling canSample() and just calling sample(...) iterating the generators?

Copy link
Contributor Author

@aditya-vk aditya-vk May 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that was my concern as well. I was wondering if changing mIndex within canSample() would make it any better but I guess that wouldn't be as clean.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think that's a desirable behavior.

Could you point me out what's your concern with the following proposal?:

for (auto i = mIndex; i < mGenerators.size(); ++i)
{
  if (mGenerators[mIndex]->sample(state))
    return true;

  mIndex++;
}

return false;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The current [updated] implementation has the same behavior as your proposal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wait, actually I think it is beneficial to get rid of canSample(), for instance if a SequentialSampler is composed of more SequentialSamplers, the current implementation would be still inefficient.

{
#ifndef NDEBUG
for (const auto& generator : mGenerators)
assert(generator->getStateSpace() == mStateSpace);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Could we add an assertion for checking nullity of the generators?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jslee02 Should it be an assertion? 🤔

Copy link
Member

@jslee02 jslee02 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this PR is almost there. :) Two minor nitpicks though.

}

// Documentation inherited
bool sample(statespace::StateSpace::State* state)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Missing override specifier.

@@ -0,0 +1,135 @@
#include "aikido/constraint/SequentialSampleable.hpp"

#include <dart/common/StlHelpers.hpp>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: I guess you meant dart/common/Memory.hpp for [dart::common::make_unique].

/// StateSpace in which the constraint operates.
statespace::StateSpacePtr mStateSpace;

/// Set of sampleables.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: set -> sequence

namespace aikido {
namespace constraint {

/// Sampleable supporting a composition of multiple sampleables which
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is confusing. Here's my stab at it:

Sampleable that wraps a sequence of Sampleables. Exhausts the current Sampleable before sampling from the next Sampleable in the sequence.

#endif
}

// Documentation inherited
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: //==============================================================================, not // Documentation inherited.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, I prefer the current formatting where the declaration happens before definition.

return mStateSpace;
}

// Documentation inherited
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: //==============================================================================, not // Documentation inherited.

return false;
}

// Documentation inherited
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: //==============================================================================, not // Documentation inherited.

return numSamples;
}

// Documentation inherited
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: //==============================================================================, not // Documentation inherited.

throw std::invalid_argument(msg.str());
}
}
// TODO (avk): Somewhere we need to give warning if initial samplers are
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good idea. I think something like "the i'th sampleable is infinite, will ignore N subsequent sampleables" would be helpful (if N > 0).

if (mGenerators[i]->getNumSamples() == NO_LIMIT)
dtwarn << "Sampleable " << i << " is infinite. "
<< "Remaining " << mGenerators.size() - i - 1
<< " sampleable(s) will potentially be ignored.";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This warning seems very informative! It would be slightly better:

  • to break once print a warning
  • not to warn if i is equal to mGenerators.size() - 1; otherwise, mGenerators.size() - i - 1 would be the greatest number of std::size_t.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree with the first point, but I think the second point should never happen since the for loop condition is i < mGenerators.size() - 1?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I missed that. Please ignore the second point.

/// sequence of sampleables
SequentialSampleGenerator(
statespace::StateSpacePtr stateSpace,
std::vector<std::unique_ptr<SampleGenerator>> generators);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Passing by const reference is not allowed because std::unique_ptr is not copiable (see this).

The codacy reports regarding smart pointers are somewhat inaccurate. 😞

//==============================================================================
SequentialSampleGenerator::SequentialSampleGenerator(
statespace::StateSpacePtr stateSpace,
std::vector<std::unique_ptr<SampleGenerator>> generators)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@brianhou brianhou merged commit fe07a1e into master May 8, 2018
@brianhou brianhou deleted the enhancement/avk/SequentialSampleable branch May 8, 2018 20:49
@jslee02 jslee02 added this to the Aikido 0.3.0 milestone May 9, 2018
gilwoolee pushed a commit that referenced this pull request Jan 21, 2019
* sequential sampleable init

* implement the functions

* make format

* add docstring

* address JS comments

* clean up getNumSamples()

* make format

* codacy issue resolved

* format + fix sample() logic

* const correctness

* make format

* iron out sample()

* simplify the logic for sample()

* remove unnecessary variables

* add nullity check

* address JS comments

* rename test file for cartesianProductSampleable

* init test files

* add some initial tests

* add more tests

* make format

* update change log

* make format

* warning in the gen. constructor

* make format

* nit warning update

* break point
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants