Navigation functionality

The Navigation functionality (veneer.navigation) lets you query and modify the current Source model using normal Python object notation. For example:

scenario.Network.Nodes[0].Name = 'Renamed node'

This notebook introduces the navigation functionality, including how it ties into the existing functionality in v.model as well as current limitations.

Setup

Start up Veneer as per usual...


In [4]:
import veneer
v = veneer.Veneer(19876)

In [2]:
v.status()


Out[2]:
{'ProjectFile': None,
 'Scenario': 'Fitzroy Rebuild 2014',
 'SourceVersion': '4.1.1.5345',
 'Version': 20180501}

Initialise a Queryable object

Queryable is the main component in veneer.navigate. By default, a Queryable points to the scenario.


In [5]:
from veneer.navigate import Queryable

In [6]:
scenario = Queryable(v)

In [7]:
scenario.Name


Out[7]:
Fitzroy Rebuild 2014

Tab completion

Queryable objects work with the tab completion in IPython/Jupyter, including, in many cases, for nested objects:


In [11]:
scenario.Network.nodes.Count


Out[11]:
2089

... However this won't work after indexing into a list or dictionary:

You can still access the properties of the list item, if you know what they're called:


In [12]:
scenario.Network.nodes[0].Name


Out[12]:
Outlet Node1

... But if you want tab completion, create a variable and run the cell that creates it. Then tab into the new variable:


In [20]:
node = scenario.Network.nodes[0]
# node.<tab>  WON'T WORK YET. You need to run this cell first

In [21]:
# Now tab completion should work should work
node.Name


Out[21]:
Outlet Node1

Accessing a particular node/link/fu/etc

The above examples start from the scenario - which works, but is tedious when you need a particular node or link (or all water users, etc).

Here, the existing functionality under v.model is has been expanded to return Queryable objects that can be used for object navigation.

All of the relevant v.model areas (link, node, catchment, etc) now have a nav_first method, which accepts the same query parameters as the other operations. For example, v.model.node.nav_first accepts the nodes and node_types query parameters, in the same way as v.model.node.get_param_values.

As always, the query parameters are available in the help, one level up:


In [52]:
v.model.node?


Type:        VeneerNodeActions
String form: <veneer.server_side.VeneerNodeActions object at 0x000001EB6EAB4358>
File:        d:\src\projects\py\veneer-py\veneer\server_side.py
Docstring:  
Queries and actions relating to nodes (incuding node models).

Query options:

* nodes - the name(s) of nodes to match when querying/configuring.

* node_types - the type(s) of nodes to match when querying/configuring

For example:

v.model.nodes.get_models(nodes='Fish River')

... So we can get a particular node, for navigation as follows.


In [53]:
callide = v.model.node.nav_first(nodes='Callide Dam')

Note: The function is called nav_first to emphasise that you will receive the first match for the query. So, if you query node_types='WaterUser', you'll get the first Water User matched.

It's likely that we'll add a more generic nav method at some point that allows you to get a Queryable capable of bulk operations.

Working with the navigation object

From the examples above, it looks like, you can work with the Queryable object as a normal Python object. In some cases you can, but not always.

For changing values in the object (ie changing the relevant property in the Source model), you can indeed set the value directly:


In [23]:
callide.Name


Out[23]:
Callide Dam

In [26]:
callide.fullSupplyLevel


Out[26]:
216.1

In [27]:
callide.fullSupplyVolume


Out[27]:
136370000

In [28]:
callide.fullSupplyVolume = 136300000

In [29]:
callide.fullSupplyVolume


Out[29]:
136300000

Note: Changing one thing may have a side effect, impacting another property:


In [31]:
callide.fullSupplyLevel


Out[31]:
216.09402900199

If a property can't be set this way, it should tell you:


In [36]:
# CAUSES EXCEPTION
#callide.fullSupplyLevel = 216

Things aren't necessarily as they seem!

The above examples suggest that the following would work:


In [38]:
# Would be nice, but doesn't work...
#callide.fullSupplyVolume = 1.1 * callide.fullSupplyVolume

The reason is, callide.fullSupplyVolume is, itself, a Queryable object:


In [39]:
callide.fullSupplyVolume.__class__


Out[39]:
veneer.navigate.Queryable

So, although it prints out as a number, it is in fact a reference to the value within the model.

If you want to actually use the value in an expression (eg to set another property), you'll need to use the `


In [41]:
callide.fullSupplyVolume = 1.1 * callide.fullSupplyVolume._eval_()

In [42]:
callide.fullSupplyVolume


Out[42]:
149930000

In [43]:
callide.fullSupplyVolume = callide.fullSupplyVolume._eval_() / 1.1

In [44]:
callide.fullSupplyVolume


Out[44]:
136300000

Evaluating a Queryable

It's not ideal to need to call ._eval_() and we plan to improve this over time.

Also, the _eval_() workaround only works for simple types - numbers, strings, booleans, etc. It doesn't work for complex objects, such as Nodes, Links and model instances.

For example you CANNOT do this at the moment:

storage = v.model.node.nav_first(nodes='my storage')
link = v.model.link.nav_first(links='some link')

# Try to set the outlet link for the storage...
storage.OutletPaths[0] = link # WILL NOT WORK!

Bulk changes

When you use the model configuration functionality under v.model every operation is a bulk operation by default - you use query parameters to limit the application:

For example, the following would retrieve Easting and Northing (really, just the node coordinates) for every node:

eastings = v.model.node.get_param_values('Node.location.E')
northings = v.model.node.get_param_values('Node.location.N')

while the following would do the same for only storages, by using query parameters:

eastings = v.model.node.get_param_values('Node.location.E',node_types='Storage')
northings = v.model.node.get_param_values('Node.location.N',node_types='Storage')

When using Queryable, everything is, currently, an operation on a single object (eg a single node):


In [50]:
callide.Node.location.E


Out[50]:
1861543.63051

However... one of the big benefits of the navigable functionality is the ability to discover the parameter name (often nested) that you need to use in a bulk operation.

When stuck for a parameter name, use a Queryable to discover it, through tab-completion and then plug the resulting path back into the v.model.X.get_param_values() call

TODO

The Queryable approach is new and will expand.

Things on the list are:

  • Ability to call methods on Queryable objects
  • Ability to use Queryable objects in expressions, assignments to other Queryables and as method parameters
  • Ability to perform bulk operations using Queryables
  • Ability to write Python loops / list comprehensions that iterate over Source collections

Other related functionality that would be nice:

  • Ability to identify functions, data sources, etc that point at a given variable. Eg instead of seeing node.Inflow = 0, see the data source that is connected.