Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Piecewise orbit model #1378

Closed
wants to merge 14 commits into from

Conversation

poneill129
Copy link
Contributor

This is intended as an extension to the existing BT model currently implemented. BT Piecewise was designed to allow a user to fit for orbital phase (T0) with user-defined MJD blocks.

Preliminary work was made to allow for other piecewise orbital parameters, but since this was a tool intended for a specific problem, the development of other piecewise orbital parameters was stopped. Though the foundations for future development in this area are present.

It feels like setting the piecewise parameters from binary_piecewise to BT_piecewise is currently a jury-rig which could do with improvements. Additional concerns surround this involves when a setup() function should be called. A better understanding of this would help performance time.

@codecov
Copy link

codecov bot commented Aug 12, 2022

Codecov Report

Merging #1378 (e02bccb) into master (4ce58a8) will increase coverage by 0.19%.
The diff coverage is 71.18%.

@@            Coverage Diff             @@
##           master    #1378      +/-   ##
==========================================
+ Coverage   62.19%   62.38%   +0.19%     
==========================================
  Files          89       91       +2     
  Lines       20215    20621     +406     
  Branches     3650     3760     +110     
==========================================
+ Hits        12572    12865     +293     
- Misses       6857     6924      +67     
- Partials      786      832      +46     
Impacted Files Coverage Δ
...nt/models/stand_alone_psr_binaries/BT_piecewise.py 68.92% <68.92%> (ø)
src/pint/models/binary_piecewise.py 74.67% <74.67%> (ø)
src/pint/models/__init__.py 100.00% <100.00%> (ø)
src/pint/models/timing_model.py 83.15% <0.00%> (+0.13%) ⬆️
.../models/stand_alone_psr_binaries/binary_generic.py 68.81% <0.00%> (+0.51%) ⬆️

Help us with your feedback. Take ten seconds to tell us how you rate us. Have a feature suggestion? Share it here.

# self.validate()

def update_binary_object(self, toas=None, acc_delay=None):
super().update_binary_object(toas=toas, acc_delay=acc_delay)
Copy link
Contributor

Choose a reason for hiding this comment

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

If all this method does is call the same method on super() can it not be omitted completely? Several other methods here seem to be in the same situation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated so only super() is called when required

self.validate()

def add_piecewise_param(self, param, param_unit, paramx, j):
if int(j) in self.get_prefix_mapping_component("X_"):
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be after the if j is None, shouldn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Reordered the statements, good catch

if param is "A1":
self.add_param(
prefixParameter(
name=param + "X_{0}".format(i),
Copy link
Contributor

Choose a reason for hiding this comment

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

Nicer to use f"X_{i}"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed string formatting as suggested (here and in other places).

frozen=False,
)
)
elif param is "T0":
Copy link
Contributor

Choose a reason for hiding this comment

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

For strings it's better to use == than is - you could have two different strings that contain T0 that were equal but not is.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

changed all instances of is to == for strings


def setup(self):
# print(self.PB)
super(BinaryBTPiecewise, self).setup()
Copy link
Contributor

Choose a reason for hiding this comment

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

Just use super().setup().

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to super().setup()

# if None in T0Xs.values():
# print("Group with non-defined T0X, applying default T0 to group")
# TODO set default T0 value
if None not in T0Xs.values():
Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't this skip defining all the parameters if any of the values is unset? That doesn't seem right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed this by assigning a global A1/T0 in place of the None it was previously being set to. The intention of this was to later re-assign None's with the global value elsewhere, but as you spotted this would break if the values are unset

self.binary_instance.add_binary_params(xr2_name, xr2_value)
self.update_binary_object()

def validate(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Are you sure there aren't any ways the new parameters could be set that would make the model unusable? validate() should check for invalid settings of the piecewise parameters. (Overlapping T0X intervals? T0X intervals with end before beginning? Any weirdness that won't work can be detected here and then you won't need to worry about it later.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Added additional checks. Will now raise a value error if:

  • Overlapping detected
  • mismatching group indexes (i.e. declaring the lower bound index as 0000 and upper bound index as 0001)
  • mismatching indexes between the piecewise param and group boundaries (i.e. declaring T0X_0000 and pieces PLB/PUB_0001)
  • If the end of a group boundary is chronologically before the start

self.extended_group_range = []
self.d_binarydelay_d_par_funcs = [self.d_BTdelay_d_par]
if t is not None:
self._t = t
Copy link
Contributor

@aarchiba aarchiba Aug 14, 2022

Choose a reason for hiding this comment

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

It's probably a good idea to set selt._t to None if t is None, rather than leaving it uninitialized. That way you get a ValueError rather than an AttributeError (the latter usually means you made a typo in the name of an attribute).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed as suggested

if input_params is not None:
if self.T0X is None:
self.update_input(input_params)
elif self.T0X is not None:
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 just else isn't it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Silly me, probably a hangover from when I thought there would be a need for extra conditions. Fixed now (along with any other iterations of this occurring)

if self.T0X is None:
self.update_input(input_params)
elif self.T0X is not None:
self.update_input()
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this doing exactly the same thing in both branches of the if statement?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, removed the condition and doesn't break the tests

super().set_param_values(valDict=valDict)
self.piecewise_parameter_loader(valDict=valDict)

def piecewise_parameter_loader(self, valDict=None):
Copy link
Contributor

Choose a reason for hiding this comment

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

It's preferable if a function name is an imperative - load_piecewise_parameters, perhaps, or setup_internal_structures or something. This function should have a docstring so you can explain what it does and when to call it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Forgot to add the docstring with the update, still to do...

if (
valDict is None
): # If there are no updates passed by binary_instance, sets default value (usually overwritten when reading from parfile)
self.T0X_arr = self.T0
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this make T0X_arr into a scalar instead of an array? Won't this break things?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Apparently it didn't, confused as to why since it's been like this through testing. Changed so it lives in a list so it matches the expected form of T0X_arr after self._t is declared

else:
piece_index = (
[]
) # iniialise array used to count the number of pieces. Operates by seaching for "A1X_i/T0X_i" and appending i to the array. Can operate if pieces are given out of order.
Copy link
Contributor

Choose a reason for hiding this comment

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

Try to avoid comments on the same line as your code, it makes for nasty formatting.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

moved remaining comments off the same line

index
) in (
piece_index
): # looping through each index in order they are given (0 -> n)
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure this is really the order you're getting. Doesn't np.unique give you the strings in sorted (lexicographical) order, regardless of the order that they were given?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So it's populating the list piece_index with all the unique piecewise parameter keys (i.e. '0000', '0001' etc). Then looks up the corresponding values from valDict using that key. It's a matching process rather than using the iteration number of the loop.

if len(self.piecewise_parameter_information) > 0:
# check = hasattr(self,"t")
# print(f"Piecewise parameter loader can see t: {check}")
if hasattr(self, "_t") is True:
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't use is True; just say if hasattr(...):

Copy link
Contributor

Choose a reason for hiding this comment

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

Also if you set this to None in __init__ you can avoid the need tor hasattr.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

removed instances of is True. There may still be some hasattr's in other conditions that could follow the same treatment

return toas


def get_toa_group_indexes(model, toas):
Copy link
Contributor

Choose a reason for hiding this comment

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

This isn't recognized as a test because it doesn't start with test_. Wouldn't it be better to just use model.which_group_is_toa_in(toas) directly?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed redundant functions like this and similar


def get_number_of_groups(model):
# returns number of groups
model.setup()
Copy link
Contributor

Choose a reason for hiding this comment

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

Likewise it's better to use model.get_number_of_groups() directly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed as part of the previous comment

@pytest.fixture()
def build_piecewise_model_with_two_pieces(build_model):
# takes the basic model frame and adds 2 non-ovelerlapping pieces to it
piecewise_model = deepcopy(build_model)
Copy link
Contributor

Choose a reason for hiding this comment

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

Fixtures always give you a fresh-created new object, so there's no need to copy it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed instances of deepcopy().

Just for reference, if I call for example:
m1 = build_model then add pieces to m1
followed by: m2 = build_model. Then attempting to add pieces results in a failure due to trying to add a piecewise parameter where one already exists. Does this look like a caching issue or is it because build_model is creating a fresh version for the test which is shared between m1 and m2?

return piecewise_model


def test_add_piecewise_parameter(build_model):
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 test is actually checking whether the model round-trips to a par file correctly? If so, maybe call the test something that says that?

Copy link
Contributor

Choose a reason for hiding this comment

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

There are round-trip tests elsewhere as well, it would be good to add a BTpiecewise model to those, to benefit from that testing code as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately I couldn't find where some of these additional tests live when updating. Still needed.

Otherwise, renamed test to something appropriate. (Likely other instances of inappropriate test names)

copy_param_dict = m3.get_params_dict(which="all")
number_of_keys = 0
comparison = 0
for key, value in param_dict.items():
Copy link
Contributor

Choose a reason for hiding this comment

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

Can you just ask assert param_dict == copy_param_dict? If this fails you'll get a list of the mismatched keys/values.

comparison = 0
for key, value in param_dict.items():
number_of_keys = number_of_keys + 1 # iterates up to total number of keys
for copy_key, copy_value in param_dict.items():
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be copy_param_dict?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good spot, yes it should be



def test_get_number_of_groups(build_piecewise_model_with_two_pieces):
# test to make sure number of groups matches with number of added piecewise parameters
Copy link
Contributor

@aarchiba aarchiba Aug 14, 2022

Choose a reason for hiding this comment

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

Can you put this information in the function name? We don't call test functions by hand so their names don't particularly need to be concise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unchanged yet, fell through the cracks while updating

np.unique(index, return_counts=True)[1][1],
]
expected_toas_in_each_group = [10, 10]
is_there_ten_toas_per_group = np.array_equal(
Copy link
Contributor

Choose a reason for hiding this comment

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

it's better to use numpy.testing.assert_array_equal, it will tell you more about the mismatch

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Implemented numpy.testing.assert_array_equal where appropriate. Additionally using numpy.testing.assert_allclose where appropriate.

m_piecewise = build_piecewise_model_with_two_pieces
toa = make_toas_to_go_with_two_piece_model
m_piecewise.setup()
rs = pint.residuals.Residuals(toa, m_piecewise)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this ever used?

def test_problematic_group_indexes_and_ranges(build_model):
# Test to flag issues with problematic group indexes
m_piecewise = build_model
with pytest.raises(Exception):
Copy link
Contributor

Choose a reason for hiding this comment

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

Please specify what kind of exception is to be raised.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Included the types of exceptions that should be raised (basically all value errors)

# Test to flag issues with problematic group indexes
m_piecewise = build_model
with pytest.raises(Exception):
assert m_piecewise.add_group_range(
Copy link
Contributor

Choose a reason for hiding this comment

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

No point asserting anything if it's supposed to raise an exception. Just run the code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

As suggested, just running the code. New function added to binary_piecewise.py called lock_groups which runs validate then setup. Avoids the need to run it after every piecewise parameter is added but must be run after adding the a set of/sets of piecewise parameters and date ranges for the boundaries.

assert m_piecewise.add_group_range(
m_piecewise.START.value, m_piecewise.FINISH.value, j=-1
)
assert m_piecewise.add_group_range(
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be in a separate with statement - the first exception exits the with.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed to only include a single assertion in a with statement. I realise most of these have been removed during the update

)


def add_groups_and_make_toas(build_model, build_piecewise_model_with_two_pieces, param):
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be three separate functions, they don't share any appreciable code.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Separated out into two functions since overlapping groups are now fail validation tests.

)


def convert_int_into_index(i):
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't there a utility function to do this in the BTpiecewise code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's removed now, all it was doing is making 1->0001 so I just declare them where necessary now rather than using a function

return truth_array_comparison


@pytest.mark.parametrize(
Copy link
Contributor

Choose a reason for hiding this comment

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

You can pass a list of functions to parametrize.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Didn't know that, thanks!

"non-complete group coverage",
],
)
def test_does_toa_lie_in_group(
Copy link
Contributor

Choose a reason for hiding this comment

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

The three branches here don't share any appreciable code so they should be three separate tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Again separated into two tests, since overlapping groups are handled differently now

rs_temp = pint.residuals.Residuals(
toas, m_piecewise_temp, subtract_mean=False
).resids_value
have_residuals_changed = np.invert(rs_temp == rs_value)
Copy link
Contributor

Choose a reason for hiding this comment

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

you can use rs_temp != rs_value

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Talk about going the long way to get the same thing! Silly oversight on my part



def get_d_delay_d_xxxx(toas, model, parameter_string):
rs_temp = pint.residuals.Residuals(toas, model, subtract_mean=False)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do you really need to construct Residuals here? Why?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It appears given the updates I don't (/perhaps never needed to), now constructing residuals is reserved for tests that actually use them.

return d_delay_temp


# @pytest.mark.parametrize("param, index, offset_size",[("T0X",0, 1e-5*u.d),#("T0X",-1, 1e-5*u.d),("A1X",0, 1e-5*ls),("A1X",-1, 1e-5*ls)])
Copy link
Contributor

Choose a reason for hiding this comment

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

You can use pytest.mark.skip with a reason to indicate that a test should be skipped.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the info, but this test has been removed and replaced now, informed by some of the test suggestions you make in the next comment!

@aarchiba
Copy link
Contributor

This looks excellent! A little code cleanup would improve it a lot. I'd also like to suggest a few additional tests:

  • Can you compare the residuals from BTPiecewise with several pieces to BT with the values appropriate for that piece? This is supposed to give exactly the same residuals but could go wrong. Checking derivatives would be good too.
  • There are some tests elsewhere that use numdifftools to check analytical derivatives against numerical. Can you integrate a BTPiecewise model into that?
  • Can you write some corner-case tests - what happens if the T0X intervals are out of order? If the end is before the beginning? If it's zero-width? If they're duplicated or overlap? Most of these should just raise exceptions, but tests can document what should happen if the user puts something wrong in.
  • Can you write a test that creates two different BTpiecewise models and calls various functions of one interlaced with calls to the other? Just to make sure there's no global caching of anything (I'm sure you're fine but it's good to check).
  • Can you write some tests that do some calculations, change some values, and do some new calculations; versus doing the second lot of calculations directly? This is to make sure that stuff doesn't linger in BTpiecewise objects from one call to the next.

@dlakaplan
Copy link
Contributor

Some discussion of the T2 model came up, and I notice that T2 implements binary parameter jumps as:

BPJEP_k Epoch of a step-jump in the binary parameters  
BPJPH_k Size of phase jump  
BTJPB_k Size of jump in orbital period  
BTJA1_k Size of jump in projected semimajor axis  
BTJECC_k Size of jump in orbital eccentricity  
BTJOM_k Size of jump in longitude of periastron  
Is that in any way compatible with how things were implemented here?

@scottransom
Copy link
Member

Hi All: What is the status of this code? Is it working? Or might it be soon? I have a new student who will be working on timing Ter5A and would love to use this, if possible.

@poneill129
Copy link
Contributor Author

poneill129 commented Mar 31, 2023

Adding some of my extra thoughts based on Anne's last block of suggestions.

Can you compare the residuals from BTPiecewise with several pieces to BT with the values appropriate for that piece? This is supposed to give exactly the same residuals but could go wrong. Checking derivatives would be good too.

  • Yes! Implemented as: test_residuals_in_pieces_are_same_as_BT_piecewise followed by the orbital parameter it is testing.
  • Also test_derivatives_in_pieces_are_same_as_BT_piecewise followed by the orbital parameter is implemented to check the derivatives correctly being calculated inside/outside of a given piece. Warning: test_derivatives_in_pieces_are_same_as_BT_piecewise_A1 is producing assertion errors if assert_array_equal is used, looks like its some form of round-off issue as the BT and BT_piecewise derivatives are effectively equal.

There are some tests elsewhere that use numdifftools to check analytical derivatives against numerical. Can you integrate a BTPiecewise model into that?

Haven't been able to spot where these live. Seems like a good idea to implement still

Can you write some corner-case tests - what happens if the T0X intervals are out of order? If the end is before the beginning? If it's zero-width? If they're duplicated or overlap? Most of these should just raise exceptions, but tests can document what should happen if the user puts something wrong in.

Implemented as validation tests which are checked in the testing script.

Can you write a test that creates two different BTpiecewise models and calls various functions of one interlaced with calls to the other? Just to make sure there's no global caching of anything (I'm sure you're fine but it's good to check).

Can you write some tests that do some calculations, change some values, and do some new calculations; versus doing the second lot of calculations directly? This is to make sure that stuff doesn't linger in BTpiecewise objects from one call to the next.

I think I've made a start on testing some of these in the final test of the script. It performs some basic arithmetic and attempts to change piecewise parameter values in two independent models based on the other model. Currently passing but I doubt this is the exact form you were suggesting. Further discussion required.

@poneill129
Copy link
Contributor Author

Replying to @dlakaplan's comment. Yes there does seem to be a natural way these two can interface. I would need to think about how I would implement this. Either as a adjust_piecewise_param(new_par_val,par_name). I'd need to check how exactly the T2 model adjusts orbital parameters but gut feeling tells me it can be made to work together.

@poneill129
Copy link
Contributor Author

Replying to @scottransom's comment. The code has received the much needed face-lift in a lot of places, and I'm in the process of trimming down an example notebook. It's not quite at the level to be integrated into PINT's tutorial notebooks just yet, but happy to distribute privately and run through how I'd use it.

@dlakaplan
Copy link
Contributor

@poneill129 : are you able to attend the weekly PINT call to discuss this?

@poneill129
Copy link
Contributor Author

@poneill129 : are you able to attend the weekly PINT call to discuss this?

Yes I will be, will this be on Thursday? If so, what time does it start?

@abhisrkckl
Copy link
Contributor

A completed version of this (#1545) has already been merged. So I am closing this.

@abhisrkckl abhisrkckl closed this Oct 4, 2023
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.

5 participants