from seeq import spy
import pandas as pd

# Set the compatibility option so that you maximize the chance that SPy will remain compatible with your notebook/script
spy.options.compatibility = 193
# Log into Seeq Server if you're not using Seeq Data Lab:
spy.login(url='http://localhost:34216', credentials_file='../credentials.key', force=False)

spy.swap

Performs “asset swapping” functionality equivalent to what you can achieve in Seeq Workbench by the “Swap in this asset…” button for an Asset found in the Data tab.

When you create a calculated item (Signal, Condition, Scalar, Threshold Metric) that is based on stored signals that are part of an asset tree, Seeq allows you to “swap” the original asset that you used for a different asset that also has all of the constituent signals (named identically).

You can then pull the data for the “swapped” calculation just like you would pull data for any other calculated item.

Let’s walk through an example.

Example

First we will query for some signals to use in our calculation and then push a calculation based on those signals.

area_a_df = spy.search({
    'Path': 'Example >> Cooling Tower 1',
    'Asset': 'Area A',
    'Name': 'Temperature'
})

pushed_items_df = spy.push(metadata=pd.DataFrame([{
            'Name': f'Added',
            'Formula': '$t + 10',
            'Formula Parameters': {'$t': area_a_df.iloc[0]}
        }, {
            'Name': f'Hot',
            'Formula': '$t > 80',
            'Formula Parameters': {'$t': area_a_df.iloc[0]}
        }, {
            'Name': f'Average on One Day in 2016',
            'Formula': "$t.average(capsule('2016-12-18'))",
            'Formula Parameters': {'$t': area_a_df.iloc[0]}
        }]), workbook='SPy Documentation Examples >> spy.swap')

pushed_items_df

Now we’ll query for the assets to swap in for the original Area A asset that we built the calculation for:

areas_def_df = spy.search({
    'Path': 'Example >> Cooling Tower 2',
    'Name': '/Area [DEF]/',
    'Type': 'Asset'
}, old_asset_format=False)

areas_def_df

Now all we have to do is execute the spy.swap() command to swap in these assets and get a bunch of new IDs we can use in spy.pull():

swapped_df = spy.swap(pushed_items_df, areas_def_df, errors='catalog')

You’ll notice a few things about the returned DataFrame:

  • The Original ID column corresponds to the calculated item for which we’re swapping assets. The values are repeated three times because we’re swapping three assets across three items (a signal, condition and scalar). That’s why there’s nine rows total.

  • Some of the rows don’t have an ID field because there were errors, which can be seen in the Result column.

  • The Swap Performed column includes precise details about what was swapped. The format is Swapped Out Asset --> Swapped In Asset.

In order to get a better look at the Result and Swap Performed columns, let’s remove some of the other columns:

pd.set_option('display.max_colwidth', None)
swapped_df[['ID', 'Name', 'Result', 'Swap Performed']]

In Seeq’s Example data, Area F does not include a Temperature signal, so spy.swap() could not create a swapped calculation for that particular asset.

For the swaps that succeeded, you can now pull the data for these swapped items like so:

# First we'll drop the rows that had errors.
successful_swaps_df = swapped_df[swapped_df['Result'] == 'Success']

spy.pull(successful_swaps_df, start='2021-01-01', end='2021-01-02')

Multi-asset Swaps

Calculations can be based on more than one asset. That means swapping is more complex, because you must specify a pairing for each swappable asset. You accomplish this using “Swap Groups”.

In the following example, we’ll push a calculation that is a summation of Compressor Power for Area A and Area B. Then we’ll swap out those assets for Areas D & E and then again for Areas D & F.

areas_df = spy.search({
    'Path': 'Example >> Cooling Tower 1',
    'Asset': '/Area [AB]/',
    'Name': 'Compressor Power'
})

added_together_df = spy.push(metadata=pd.DataFrame([{
    'Name': 'Added Together',
    'Formula': '$a + $b',
    'Formula Parameters': {
        '$a': areas_df[areas_df['Asset'] == 'Area A'],
        '$b': areas_df[areas_df['Asset'] == 'Area B']
    }
}]), workbook='SPy Documentation Examples >> spy.swap')

added_together_df = spy.search({
    'Name': 'Added Together'
}, include_swappable_assets=True, workbook='SPy Documentation Examples >> spy.swap')

added_together_df

Notice that we used the spy.search(include_swappable_assets=True) flag, which means that the added_together_df DataFrame includes a Swappable Assets column where each cell is an embedded DataFrame detailing the assets upon which the calculation is based:

swappable_assets_df = added_together_df.iloc[0]['Swappable Assets']
swappable_assets_df

That’s what we need in order to specify our “swap groups”. Now we have to create a DataFrame of assets with a Swap Group and Swap Out columns to achieve the pairing we talk about above:

# Grab a DataFrame that contains Areas D, E and F
cooling_tower_2_df = spy.search({
    'Path': 'Example >> Cooling Tower 2',
    'Type': 'Asset'
}, old_asset_format=False).sort_values(by='Name')

# Create a DataFrame that includes the combination of areas:
swap_groups_df = pd.DataFrame([
    cooling_tower_2_df.iloc[0],  # Area D
    cooling_tower_2_df.iloc[1],  # Area E

    cooling_tower_2_df.iloc[0],  # Area D
    cooling_tower_2_df.iloc[2]]) # Area F

# Now we'll specify our "Swap Group" and "Swap Out" columns. The Swap Group column can be any signifier you wish to use
# to group the swaps together. Any rows with the same value for the Swap Group will be grouped together for swapping
# purposes.
swap_groups_df['Swap Group'] = [1, 1, 2, 2]

# The "Swap Out" column can be an asset ID string, the latter part of an asset's path, or a DataFrame row for an asset.
# It must be for an asset that is part of the "Swappable Assets" found above.
swap_groups_df['Swap Out'] = [
    swappable_assets_df.iloc[0],  # Area A
    swappable_assets_df.iloc[1],  # Area B

    swappable_assets_df.iloc[0],  # Area A
    swappable_assets_df.iloc[1],  # Area B
]

swap_groups_df

Now we swap! Note that we’re going to have two new swapped calculations – one for each Swap Group. You can see that there are a couple of actions noted in the Swap Performed column (separated by a \n line break).

swapped_df = spy.swap(added_together_df, swap_groups_df, old_asset_format=False)

# Use a specific set of display properties so that the linebreaks are rendered correctly
display(swapped_df[['ID', 'Name', 'Result', 'Swap Performed']].style.set_properties(**{
    'text-align': 'left',
    'white-space': 'pre-wrap',
}))

We can pull just like we did before:

spy.pull(swapped_df, start='2021-01-01', end='2021-01-02')

Multi-level Swaps

If you have a hierarchy that contains “commonly named” nodes at the bottom of the tree, you’ll need to do a multi-level swap. Here’s an example where “Raw” and “Cleansed” are categorical child “assets” that are common between the uniquely-named assets (Area A & Area B):

Multi-level Swap Example >> Area A >> Raw      >> Temperature
                                      Cleansed >> Temperature
                            Area B >> Raw      >> Temperature
                                      Cleansed >> Temperature

You must specify the uniquely-named assets as your “Swap Out” values, and SPy will figure out how to perform the swap at the lower level. Here’s an example where the categories are Raw and Cleansed:

# First we have to create the asset tree we want from Example data.

area_ab_df = spy.search({
    'Path': 'Example >> Cooling Tower 1',
    'Asset': '/Area [AB]/',
    'Name': 'Temperature'
}, old_asset_format=False)

raw_signals = area_ab_df.copy()
raw_signals['Path'] = 'Multi-level Swap Example >> ' + raw_signals['Asset']
raw_signals['Asset'] = 'Raw'
raw_signals['Reference'] = True

cleansed_signals = area_ab_df.copy()
cleansed_signals['Path'] = 'Multi-level Swap Example >> ' + cleansed_signals['Asset']
cleansed_signals['Asset'] = 'Cleansed'
cleansed_signals['Reference'] = True

combined_signal = pd.DataFrame([{
    'Name': 'Raw and Cleansed Averaged',
    'Type': 'Signal',
    'Formula': '($r + $c) / 2',
    'Formula Parameters': {
        '$r': {'Path': 'Multi-level Swap Example >> Area A', 'Asset': 'Raw',      'Name': 'Temperature'},
        '$c': {'Path': 'Multi-level Swap Example >> Area A', 'Asset': 'Cleansed', 'Name': 'Temperature'}
    }
}])

pushed_items_df = spy.push(metadata=pd.concat([raw_signals, cleansed_signals, combined_signal], ignore_index=True),
                           workbook='SPy Documentation Examples >> spy.swap')

pushed_items_df[['ID', 'Type', 'Path', 'Asset', 'Name']]
raw_plus_cleansed = pushed_items_df[pushed_items_df['Name'] == 'Raw and Cleansed Averaged']
area_a_df = pushed_items_df[(pushed_items_df['Type'] == 'Asset') & (pushed_items_df['Name'] == 'Area A')]
area_b_df = pushed_items_df[(pushed_items_df['Type'] == 'Asset') & (pushed_items_df['Name'] == 'Area B')]

# Create a Swap In / Swap Out pairing that uses the unique level of the tree, namely:
#  Swap Out:  test_multilevel_swap >> Example >> Cooling Tower 1 >> Area A
#  Swap In:   test_multilevel_swap >> Example >> Cooling Tower 1 >> Area B

assets_df = area_b_df.copy()                  # Swap In
assets_df['Swap Out'] = [area_a_df.iloc[0]]   # Swap Out

swap_results = spy.swap(raw_plus_cleansed, assets_df)

display(swap_results[['ID', 'Name', 'Swap Performed']].style.set_properties(**{
    'text-align': 'left',
    'white-space': 'pre-wrap',
}))

Detailed Help

All SPy functions have detailed documentation to help you use them. Just execute help(spy.<func>) like you see below.

help(spy.swap)