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

Wrapper of options #471

Merged
merged 26 commits into from
Mar 1, 2024
Merged

Wrapper of options #471

merged 26 commits into from
Mar 1, 2024

Conversation

ta440
Copy link
Collaborator

@ta440 ta440 commented Jan 9, 2024

This adds the ability to apply the different wrapper options of EmbeddedDG and Recovery to different tracers/prognostic variables in the transport step. This will enable tracers to live in different spaces and will fix a current compatibility issue where a DG and theta tracer can't both be used within the CoupledTransportEquation with respective sublimiters, as the ThetaLimiter requires the use of EmbeddedDG.

This is implemented by defining a new dictionary wrapper 'MixedOptions' which will store the individual wrapper options of the transported variables. Tracers have different options, or may have none defined at all. Currently, SUPG has not been implemented as an option for a tracer.

A new test file, 'test_mixed_fs_options.py' is made to test combinations of these different options for two tracers solved with a CoupledTransportEquation. Limiters are also used on all these tests. As this new script includes the DG1-DG1_equispaced combination, this test has been removed from the 'test_limiters.py' file.

@ta440 ta440 marked this pull request as ready for review January 23, 2024 01:16
Copy link
Contributor

@tommbendall tommbendall left a comment

Choose a reason for hiding this comment

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

Thanks very much for doing this Tim, I think this will enable us to lots of exciting things!

I think the methods for the MixedOptions wrapper look perfect and I think it's the right thing to have a new test, which also looks good.

However (as I've written in a detailed comment) I think we actually might want to separate the new object into two objects:

  1. MixedFSOptions to live in configuration.py
  2. MixedFSWrapper to live in wrappers.py and would be very close to what you've implemented already

The reason for this is that it keeps the code structure used for the existing Options/Wrappers.

My other thought (as I've written in another comment) is about whether we can avoid having the mixed_options property for wrapper objects?

I'm very happy to discuss these things offline.

self.wrapper = RecoveryWrapper(self, options)
elif self.wrapper_name == "supg":
self.wrapper = SUPGWrapper(self, options)
if type(options) == MixedOptions:
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if type(options) == MixedOptions:
if type(options) is MixedOptions:

I think when type checking, it's slightly better to use is rather than ==

elif self.wrapper_name == "supg":
self.wrapper = SUPGWrapper(self, options)
if type(options) == MixedOptions:
self.wrapper = options
Copy link
Contributor

Choose a reason for hiding this comment

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

Not that what you've done isn't a sensible way of implementing this, but it's occurred to me that it isn't quite consistent with how the existing wrappers work.

At the moment we have two broad types of objects: Options and Wrappers, and I think what we've done here is mix up the two a bit.

So now I'm thinking the best way of mirroring this structure for mixed function spaces would be to have:

  1. MixedFSOptions which is defined in configuration.py and is essentially a glorified dictionary
  2. MixedFSWrapper which is defined in wrappers.py and has the same methods as other Wrapper objects

assert isinstance(self.options, RecoveryOptions), \
'Embedded DG wrapper can only be used with Recovery Options'
'Recovery wrapper can only be used with Recovery Options'
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for correcting this!

else:
x_in_sub.assign(field)

def post_apply(self, x_out):
Copy link
Contributor

Choose a reason for hiding this comment

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

This method looks exactly right to me

self.x_out = Function(self.function_space)

def pre_apply(self, x_in):
"""
Copy link
Contributor

Choose a reason for hiding this comment

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

This method looks exactly right to me

@@ -33,6 +33,9 @@ def __init__(self, time_discretisation, wrapper_options):
self.time_discretisation = time_discretisation
self.options = wrapper_options
self.solver_parameters = None
self.idx = None
self.mixed_options = False
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 a little bit uncertain about having a general Wrapper object know about mixed_options...

It feels like the inheritance of properties could be the wrong way around, as a MixedFSWrapper has several subwrappers, but the subwrappers then need to know that they are subwrapper. Ideally it would be better if they don't know that they are subwrappers!

Looking below at why you've had to do this, it's to set the function space. I'm just wondering if we can find a cleaner way to set original_space? Maybe idx could be an argument to the __init__ or setup method of the base Wrapper class?

Copy link
Contributor

@tommbendall tommbendall left a comment

Choose a reason for hiding this comment

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

Thanks again for this Tim. I think we're nearly there.

I understand why you needed to introduce the tracer_fs to the other wrappers. I think we could go a bit further with this by:

  • renaming it as original_space (since it might not always be used with tracers!)
  • having it set more explicitly through the setup method, which will help make the code clearer

@@ -161,13 +169,19 @@ class RecoveryWrapper(Wrapper):
def setup(self):
"""Sets up function spaces and fields needed for this wrapper."""

print(self.options)
Copy link
Contributor

Choose a reason for hiding this comment

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

This just needs removing!

@@ -33,6 +33,7 @@ def __init__(self, time_discretisation, wrapper_options):
self.time_discretisation = time_discretisation
self.options = wrapper_options
self.solver_parameters = None
self.tracer_fs = None
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
self.tracer_fs = None
self.original_space = None


self.residual = self.wrapper.label_terms(self.residual)
subwrapper.setup()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
subwrapper.setup()
subwrapper.setup(self.equation.spaces[field_idx])

field_idx = equation.field_names.index(field)

# Store the original space of the tracer
subwrapper.tracer_fs = self.equation.spaces[field_idx]
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
subwrapper.tracer_fs = self.equation.spaces[field_idx]

map_if_true=replace_test_function(new_test_mixed))

else:
self.wrapper.setup()
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
self.wrapper.setup()
self.wrapper.setup(self.fs)

I think this should be self.fs but I'm not sure...? Maybe it is just None

Copy link
Collaborator Author

@ta440 ta440 Feb 28, 2024

Choose a reason for hiding this comment

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

I think we would want this to be None, as we want self.original_space=None when not using the mixed wrapper.

original_space = self.tracer_fs
else:
original_space = self.time_discretisation.fs

Copy link
Contributor

Choose a reason for hiding this comment

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

Again this snippet changes to

self.original_space = original_space

@@ -184,10 +198,17 @@ def setup(self):
# Internal variables to be used
# -------------------------------------------------------------------- #

self.x_in_tmp = Function(self.time_discretisation.fs)
if self.tracer_fs 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.

Suggested change
if self.tracer_fs is not None:
if self.original_space is not None:

@@ -184,10 +198,17 @@ def setup(self):
# Internal variables to be used
# -------------------------------------------------------------------- #

self.x_in_tmp = Function(self.time_discretisation.fs)
if self.tracer_fs is not None:
self.x_in_tmp = Function(self.tracer_fs)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
self.x_in_tmp = Function(self.tracer_fs)
self.x_in_tmp = Function(self.original_space)

self.x_in = Function(self.function_space)
self.x_out = Function(self.function_space)
if self.time_discretisation.idx is None:

if self.tracer_fs 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.

Suggested change
if self.tracer_fs is not None:
if self.original_space is not None:

if self.time_discretisation.idx is None:

if self.tracer_fs is not None:
self.x_projected = Function(self.tracer_fs)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
self.x_projected = Function(self.tracer_fs)
self.x_projected = Function(self.original_space)

@ta440
Copy link
Collaborator Author

ta440 commented Feb 28, 2024

Thanks for the comments, Tom!
By having the original_space set in the setup, I assume you mean for the individual wrappers (EmbeddedDG, Recovery), as if it was in the original wrapper object, we still have to pass original space as an argument into the individual wrapper instances. If so, I don't think we need original_space to actually be a property of wrappers, as we pass it in if using MixedFSWrapper, and it will default to None if not.
I think this requires the argument to the setup() function to be called something other than original_space, to avoid confusion when we define it later on. I've put in previous_space as a placeholder, but can change it to a better name?
Please let me know if this wasn't the implementation you were thinking of! Perhaps there is a simpler way to set self.original_space before performing the subwrapper.setup()?

Copy link
Contributor

@tommbendall tommbendall left a comment

Choose a reason for hiding this comment

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

Thanks Tim for doing all of this! I think this is ready now.

@tommbendall tommbendall merged commit 6d76ca7 into main Mar 1, 2024
4 checks passed
@tommbendall tommbendall deleted the wrapper_of_options branch March 1, 2024 10:33
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.

2 participants