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

Ref/properties #70

Merged
merged 35 commits into from
Sep 19, 2017
Merged

Ref/properties #70

merged 35 commits into from
Sep 19, 2017

Conversation

lheagy
Copy link
Member

@lheagy lheagy commented Sep 1, 2017

Implement properties in discretize

Use properties to define the properties of a class that are necessary to completely define the object. This allows us to serialize, save and load meshes (also copy!)

image

todos

  • test on cyl mesh
  • test on tree mesh
  • test on curvi mesh

@rowanc1, @fwkoch: if you guys would be willing to do a review, I would appreciate another set of eyes

Related issues

@lheagy lheagy requested review from rowanc1 and fwkoch September 1, 2017 15:50
@codecov
Copy link

codecov bot commented Sep 1, 2017

Codecov Report

Merging #70 into dev will increase coverage by <.01%.
The diff coverage is 86.55%.

Impacted file tree graph

@@            Coverage Diff             @@
##              dev      #70      +/-   ##
==========================================
+ Coverage   76.98%   76.98%   +<.01%     
==========================================
  Files          17       17              
  Lines        4797     4858      +61     
==========================================
+ Hits         3693     3740      +47     
- Misses       1104     1118      +14
Impacted Files Coverage Δ
discretize/View.py 4.91% <0%> (ø) ⬆️
discretize/CurvilinearMesh.py 99.4% <100%> (+0.02%) ⬆️
discretize/MeshIO.py 83.56% <100%> (+0.47%) ⬆️
discretize/utils/meshutils.py 69.35% <100%> (ø) ⬆️
discretize/BaseMesh.py 83.81% <77.19%> (-1.77%) ⬇️
discretize/TensorMesh.py 90.9% <85.71%> (-1.13%) ⬇️
discretize/utils/__init__.py 85.71% <85.71%> (+1.09%) ⬆️
discretize/CylMesh.py 84.61% <90.9%> (-0.04%) ⬇️
discretize/TreeMesh.py 82.8% <94.44%> (+0.04%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4ccd933...a75afa5. Read the comment docs.

@@ -21,9 +21,6 @@ discretize
:target: https://www.codacy.com/app/lindseyheagy/discretize?utm_source=github.com&amp;utm_medium=referral&amp;utm_content=simpeg/discretize&amp;utm_campaign=Badge_Grade
:alt: codacy status

.. image:: https://www.quantifiedcode.com/api/v1/project/97126041a7e7400fbf1c5555bed514dc/badge.svg
Copy link
Member Author

Choose a reason for hiding this comment

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

we weren't really using this and they discontinued it


class BaseRectangularMesh(BaseMesh):
"""BaseRectangularMesh"""
def __init__(self, n, x0=None):
BaseMesh.__init__(self, n, x0)
BaseMesh.__init__(self, n, x0=x0)
Copy link
Member Author

Choose a reason for hiding this comment

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

this isn't strictly necessary. just for clarity

assert (type(x) == list or isinstance(x, np.ndarray)), "x must be either a list or a ndarray"
assert xType in ['CC', 'N', 'F', 'Fx', 'Fy', 'Fz', 'E', 'Ex', 'Ey', 'Ez'], "xType must be either 'CC', 'N', 'F', 'Fx', 'Fy', 'Fz', 'E', 'Ex', 'Ey', or 'Ez'"
assert outType in ['CC', 'N', 'F', 'Fx', 'Fy', 'Fz', 'E', 'Ex', 'Ey', 'Ez'], "outType must be either 'CC', 'N', 'F', Fx', 'Fy', 'Fz', 'E', 'Ex', 'Ey', or 'Ez'"
allowed_xType = [
Copy link
Member Author

Choose a reason for hiding this comment

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

whitespace cleanup

@@ -25,7 +27,62 @@ def normalize3D(x):
return x/np.kron(np.ones((1, 3)), utils.mkvc(length3D(x), 2))


class CurvilinearMesh(BaseRectangularMesh, DiffOperators, InnerProducts, CurviView):
class Array(properties.Array):
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the implementation of an array that doesn't care about shape (I allow shape to be '*' which could be a 1D, 2D or 3D array). See discussion on seequent/properties#192

Copy link
Member

Choose a reason for hiding this comment

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

Union property could work well - see my comment on seequent/properties#192

min=0
)

cells = properties.Set(
Copy link
Member Author

Choose a reason for hiding this comment

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

I would prefer to keep this a private variable _cells. See discussion on seequent/properties#191

@@ -1,8 +1,8 @@
/* Generated by Cython 0.24.1 */
/* Generated by Cython 0.25.2 */
Copy link
Member Author

Choose a reason for hiding this comment

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

If you prefer, I can remove this. Updated cython cc @rowanc1

@@ -19,7 +19,6 @@ def setUp(self):
def test_vectorN_2D(self):
testNx = np.array([3, 4, 5, 6])
testNy = np.array([5, 6, 8])

Copy link
Member Author

Choose a reason for hiding this comment

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

just whitespace in this file

Copy link
Member

@rowanc1 rowanc1 left a comment

Choose a reason for hiding this comment

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

Looking good. A few minor comments on the min/max side of things.

n = properties.Array(
"number of cells in each direction (dim, )",
dtype=int,
required=True,
Copy link
Member

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.

Actually, that is only for lists. Perhaps we could change properties to have a set of tuples for defining the shape?

shape={(1,), (2,), (3,)}??

Copy link
Member Author

Choose a reason for hiding this comment

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

That would be handy

@properties.validator('n')
def check_n_shape(self, change):
change['value'] = np.array(change['value'], dtype=int).ravel()
if len(change['value']) > 3:
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 can be solved by min/max length in the list property.

Copy link
Member Author

Choose a reason for hiding this comment

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

This isn't a list (it is an array). Which I don't think has a min/max length

"""

# Properties
n = properties.Array(
Copy link
Member

Choose a reason for hiding this comment

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

Can we change this later? Having it as a public property seems to suggest that we can change it....

Copy link
Member Author

@lheagy lheagy Sep 2, 2017

Choose a reason for hiding this comment

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

the user shouldn't be changing it. It might, for example, get updated if you refine an octree mesh.
Properties doesn't currently allow private variables. see seequent/properties#191

Copy link
Member Author

Choose a reason for hiding this comment

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

agreed though - ideally, this should remain a private variable

Array(
"node locations in a dimension",
shape='*'
),
Copy link
Member

Choose a reason for hiding this comment

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

min=2 and max=3 length on the list?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is an array, so we can't, but I will implement the Union Property as suggested by @fwkoch

Copy link
Member

@fwkoch fwkoch left a comment

Choose a reason for hiding this comment

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

Overall, I love the properties stuff - let me know if you have questions on any of my comments. I also had a few non-properties questions, mostly minor things. Also, I really like the whitespace cleanup everywhere!!

"cell number",
min=0
),
default=set()
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 you should use default=set - this dynamic default will get called every time, creating a new set for each TreeMesh instance. The way you have it now, this specific set() will be shared across all instances, I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

ah, thanks

)

@properties.validator('x0')
def check_x0_vs_n(self, change):
Copy link
Member

Choose a reason for hiding this comment

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

A couple picky thoughts on this:

  • self.n could be None if someone deletes it or sets it to undefined. len(None) would give an ugly error. Possibly worth adding an extra sanity check for that?
  • Also, possibly worth running this check in the other direction (ie you validate x0 vs n, but not n vs x0) - may come up depending on how someone is using this.

@@ -44,7 +60,7 @@ def dim(self):
:rtype: int
:return: dim
"""
return self._dim
return len(self.n)
Copy link
Member

Choose a reason for hiding this comment

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

For all of these, actually, possibly worth checking if things (self.n, self.dims, etc.) are None.

@@ -25,7 +27,62 @@ def normalize3D(x):
return x/np.kron(np.ones((1, 3)), utils.mkvc(length3D(x), 2))


class CurvilinearMesh(BaseRectangularMesh, DiffOperators, InnerProducts, CurviView):
class Array(properties.Array):
Copy link
Member

Choose a reason for hiding this comment

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

Union property could work well - see my comment on seequent/properties#192

assert nodes_i.shape == nodes[0].shape, ("nodes[{0:d}] is not the "
"same shape as nodes[0]"
.format(i))
def __init__(self, nodes=nodes, **kwargs):
Copy link
Member

Choose a reason for hiding this comment

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

Looks like you are using nodes, the properties.List, as the default value...? Seems fishy... though I may be missing something.

Copy link
Member Author

Choose a reason for hiding this comment

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

thanks, this is a typo.


@properties.validator('cartesianOrigin')
def check_cartesian_origin_shape(self, change):
change['value'] = np.array(change['value'], dtype=float).ravel()
Copy link
Member

Choose a reason for hiding this comment

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

This is cool! I like seeing that you are using the validator to (possibly) change the value.


else:
assert 'h' in kwargs.keys(), 'cell widths must be provided'
h = kwargs.pop('h')
Copy link
Member

Choose a reason for hiding this comment

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

This is a little funny - on first impulse, my question was "Why does h get special treatment as an arg but not as a kwarg?" Looking into it further, though, as long as h is provided as an arg or a kwarg, it will set h_in. So really, this line will never be reached; if the else clause is reached, the assert will fail (I think at least).

Silly example:
image

Copy link
Member Author

Choose a reason for hiding this comment

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

It is a bit odd, and maybe you have a suggestion for me on how this might be made more transparent?

The reason for this is to support two uses:

  1. creating the mesh with h, x0 as args (or h_in). This is currently how most people work with the API
  2. the reason for having h also allowed as a kwarg is so that I can create a re-create a mesh from a serialized mesh

image

where load_mesh deserializes the mesh (https://github.com/simpeg/discretize/blob/ref/properties/discretize/utils/meshutils.py#L17)

def load_mesh(filename):
    """
    Open a json file and load the mesh into the target class
    As long as there are no namespace conflicts, the target __class__
    will be stored on the properties.HasProperties registry and may be
    fetched from there.
    :param str filename: name of file to read in
    """
    with open(filename, 'r') as outfile:
        jsondict = json.load(outfile)
        data = properties.HasProperties.deserialize(jsondict, trusted=True)
    return data

and for this I am under the impression that all properties in the serialized json needed to be able to be read in as kwargs.

Copy link
Member

@fwkoch fwkoch Sep 5, 2017

Choose a reason for hiding this comment

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

I see the need for h as both arg and kwarg - I think that is still covered.

This exposes a bit of a subtlety with how Python treats args/kwargs. If a named arg variable shows up anywhere in the kwargs (and is not provided in the args) it is assigned to the associated arg (and that variable does not show up in kwargs internal to the function).

If you play around with this:

class Blah(object):
    
    def __init__(self, h=None, x0=None, **kwargs):
        print(h)
        print(x0)
        print(kwargs)

You will find for all of these, kwargs = {'something_else': 2} (and h/x0 are assigned separately):

Blah(0, 1, something_else=2)
Blah(h=0, x0=1, something_else=2)
Blah(something_else=2, x0=1, h=0)

This errors (h assigned twice):

Blah(0, x0=1, something_else=2, h=3)

So bottom line, even if it is coming in as a deserialized dict, the argument will still be moved to the right place. Does that make sense...?


# Ensure h contains 1D vectors
self._h = [utils.mkvc(x.astype(float)) for x in h]
n = np.array([x.size for x in h])
Copy link
Member

Choose a reason for hiding this comment

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

Should this be else?

"""
Open a json file and load the mesh into the target class

As long as there are no namespace conflicts, the target __class__
Copy link
Member

Choose a reason for hiding this comment

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

If you want, you can narrow down your namespace a little by overriding the _REGISTRY on your BaseMesh class (ie just add _REGISTRY = {} to the class). Then you could say data = BaseMesh.deserialize(...) and it will only look at classes that inherit from BaseMesh (and therefore are on the BaseMesh registry, rather than the HasProperties registry).

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, cool, thanks!

@lheagy
Copy link
Member Author

lheagy commented Sep 11, 2017

Properties can now be private (see seequent/properties#194, thanks @fwkoch!!), so...

  • on the BaseMesh n is a private property (_n)
  • on the TreeMesh, levels and cells are private (_levels and _cells)

and Arrays can now be given a set for the shape (see seequent/properties#195), so

  • on the CurviMesh, nodes is a list of arrays.

I am happy with the state of this pr at this point, @rowanc1, if you are as well, I will do a beta release and merge to dev.

Copy link
Member

@rowanc1 rowanc1 left a comment

Choose a reason for hiding this comment

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

Looks good to me! Thanks @lheagy this is awesome.

@@ -290,11 +351,33 @@ def projectEdgeVector(self, eV):
), 'eV must be an ndarray of shape (nE x dim)'
return np.sum(eV*self.tangents, 1)

def save(self, filename='mesh.json', verbose=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 might want to be on a base class for simpeg in future (or in the utils of discretize)? It looks like a pretty generic function.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed. Once we have injected properties elsewhere in SimPEG we can abstract this up.

"You cannot change types when reshaping."
)
assert xType in outType, "You cannot change type of components."

if type(x) == list:
for i, xi in enumerate(x):
Copy link
Member

Choose a reason for hiding this comment

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

It looks like this never gets hit by tests. Did it before?

Copy link
Member Author

Choose a reason for hiding this comment

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

The only thing I changed above is whitespace. This particular line has not changed at all in this pr

"List of arrays describing the node locations",
prop=properties.Array(
"node locations in an n-dimensional array",
shape={('*', '*'), ('*', '*', '*')}
Copy link
Member

Choose a reason for hiding this comment

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

Cool!

@lheagy lheagy merged commit 267c9fd into dev Sep 19, 2017
@lheagy lheagy deleted the ref/properties branch September 19, 2017 08:35
@lheagy lheagy mentioned this pull request Sep 21, 2017
3 tasks
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