Jump to content

Chris Harp

Seeq Team
  • Posts

    12
  • Joined

  • Last visited

  • Days Won

    3

Chris Harp last won the day on February 9

Chris Harp had the most liked content!

1 Follower

Recent Profile Visitors

The recent visitors block is disabled and is not being shown to other users.

Chris Harp's Achievements

  1. Hi Trevor, Here's a topic that covers how to hold the last value from infrequently updated signals. Let me know if you have any other questions.
  2. Hello Manoel, To compare values at the start of a capsule, you would use Signal from Condition with a summary statistic "Value at Start" for each variable and use the "Start" placement for the timestamp. This will create two discrete signals at the starting time of your capsule. To create a condition that compares the two values, use Formula. You cannot directly compare discrete values in Formula. You get an error stating "x must be continuous signal at $signal1". Using the resampleHold function, you hold the discrete value for the length of your desired condition. For example, if you want your comparison condition duration to be 1 hour, you would use: $signal2.resampleHold(1h, 1min) < $signal1.resampleHold(1h, 1min) If you wish the have the comparison condition capsule duration be equal to the same duration as the original condition capsule, you could add a .touches() function where the original condition touches the comparison condition. $comparison = $signal2.resampleHold(1h, 1min) < $signal1.resampleHold(1h, 1min) $originalCondition.removeLongerThan(40h).touches($comparison) Alternatively, you could change the timestamp placement in your signal from conditions to Duration for both and then use value search or formula to create the comparison conditions. This will create a conditions equal to the length of the original condition. Please let me know if you have any other questions. Chris
  3. Hi Joseph, Thank you for letting us know about this issue. Can you submit this issue here? https://seeq.atlassian.net/servicedesk/customer/portal/3 We can link this issue to a ticket where you'll be notified when a fix has been released. Chris
  4. To create a 1 year period that resets daily at midnight, you can use the following formula: periods(1y, 1d, '2024-01-01', 'America/Chicago') When creating your rolling average calculation, use the endKey() in your aggregate function. $signal.aggregate(average(),$1yearPeriod,endkey()) Let me know if you have any questions.
  5. Hi Andrea. This feature is not currently available but is currently in development. If you wish to be notified when this feature is released, you can create a support ticket under Analytics Help and we will link your ticket to the Feature Request. https://seeq.atlassian.net/servicedesk/customer/portal/3
  6. Yes, in both cases, the signal held will be updated to reflect the new data point.
  7. Hi Paul, In R60+, you can use the resampleHold() function. Here is a link to the Knowledge Base article. https://support.seeq.com/kb/latest/cloud/optimizing-for-infrequently-updating-data If you are using an older version, you can splice a signal that is equal to the last value. $capsule = capsule(now()-7d,now()) $last_value = $signal.toScalars($capsule).last().tosignal() $signal.forecastSplice($last_value) You may need to modify how far back from now in your capsule definition to capture the last signal.
  8. Hi João, To train your model to be more accurate when the tank level is decreasing and ignore periods where the tank level is fairly constant, we need to create conditions for when the tank is rapidly decreasing. We can use that condition to train your model and then splice in the forecast data when the condition is true (e.g., the tank level is rapidly decreasing). First is to create a derivative from a smoothed signal. Using Formula: $signal.agileFilter(2min).derivative('h') Adjust the agileFilter time to makes sense for you data. You don't want a lot of noise in the derivative signal but you also don't want to lose too much resolution. Create a condition using Value Search or Formula. ($derivative_signal < -1) .merge(1h) .removeShorterThan(1h) We know have a condition when the tank level is decreasing at a rate of -1%/hr. It will ignore gaps shorter than an hour and also capsules shorter than 1 hour. We now need to create a time signal to use in our prediction of the level in Formula. By using the growEnd(1d), we'll be able to use this Signal in forecasting our tank level. timeSince($tank_decrease_condition.growEnd(1d), 1h) We want to use this timeSince signal and our condition in our Prediction. Using the advanced dropdown, we can limit our training set to when our tank is rapidly decreasing by using our Derivative Condition. However, by having the timeSince extend beyond the condition by using the growEnd function, we can use this signal to forecast the level. The last step is to create a signal using Formula which will splice the prediction signal on our tank level signal. $tank_level.forecastSplice($tank_level_prediction) This will splice the forecast to your signal. You can adjust the duration as necessary to fit your use case. Hope this helps.
  9. Instead of: for workbook in workbooks: print(workbook.Name) try: print(workbooks["Name"])
  10. Troubleshooting common errors when using Asset Templates: Spy.Assets.Build Error 1: No matching metadata row found Error - Key Phrase: "No matching metadata row found" for Temperature Unique Raw Tag on Level_4 class Fix - Modify the search criteria of the attribute shown in the error Error 2: Component dependency not built (calculated tag) Errors - Key phrases: Component Dependency not built - A calculated signal could not be created because the formula parameter referenced was not created. Attribute dependency not built - No matching metadata row found for the referenced formula parameter. Fix - Modify the search criteria of the attributes referenced by the calculated signals Error 3: Component dependency not built (branch not built) Error: Key Phrases: name 'Level 4' is not defined. Unit Component [on AssetTreeName class] component dependency not built. This is because the referenced template in def Unit_Component in the AssetTreeClass does not exist. The class name for the child branch is actually "Not_Level_4". Fix- Change the template to match the actual class name. Example: Code causing error: class AssetTreeName(Asset): @Asset.Component() def Unit_Component(self, metadata): return self.build_components( template=Level_4, metadata=metadata, column_name='Level_3' ) class Not_Level_4(Asset): @Asset.Attribute() def Temperature_Unique_Raw_Tag(self, metadata): return metadata[metadata['Name'] == 'Temperature Unique Raw Tag'].iloc[[0]] Corrected Code: class Level_4(Asset): @Asset.Attribute() def Temperature_Unique_Raw_Tag(self, metadata): return metadata[metadata['Name'] == 'Temperature Unique Raw Tag'].iloc[[0]] Error 4: Multiple attributes returned Error - Key Phrase: "Multiple attributes returned" - This indicates that your search criteria for the attribute returned multiple results. In this case, there a match in ">> Area A" and a match in ">> Area A >> Area A-Duplicate". Fix - Use .iloc[[0]] at the end of the metadata search criteria to select the first match. Original: metadata[metadata['Name'].str.contains(r'(?:Temperature)')] Revised: metadata[metadata['Name'].str.contains(r'(?:Temperature)')].iloc[[0]] Alternatively, you can add more criteria to the search to filter down to the desired tag: metadata[metadata['Name'].str.contains(r'(?:Temperature)')] & metadata[metadata['Level_4'].str.contains(‘Duplicate’) Error 5: ‘class name’ object has no attribute ‘attribute method name’ for calculated attributes Error: Key Phrases: "in Temperature_Cond ‘Formula Parameters’ lies the error. For the formula parameter, we have misspelled the attribute method name in the Level_4 class. Fix – check for misspelled methods or correct method is referenced Error 6: ‘class name’ object has no attribute ‘attribute name’ for roll up calculations Error: Key Phrases: "Cooling_Tower_Max_Temperature" is causing the error. For the roll up calculation, we are trying to reference the self.Temperature_Unique_Raw_Tag() which is in Level_4 which is the child branch. Since this method does not reside in the AssetTreeName class, it cannot find it. Example: Code causing error: class AssetTreeName(Asset): @Asset.Component() def Unit_Component(self, metadata): return self.build_components( template=Level_4, # This the name of the child branch class metadata=metadata, column_name='Level_3' # Metadata column name of the desired asset; Ex: Area A ) # Example Roll Up Calculation @Asset.Attribute() def Cooling_Tower_Max_Temperature(self, metadata): return self.Temperature_Unique_Raw_Tag().roll_up('maximum') Fix - Use the method that builds the child branch (Unit_Component) in the AssetTreeName class and then use the pick function using the friendly name of the tag to be used in the calculation. # Example Roll Up Calculation @Asset.Attribute() def Cooling_Tower_Max_Temperature(self, metadata): return self.Unit_Component().pick({ 'Name': 'Temperature Possible Duplicates Raw Tag' }).roll_up('maximum') Error 7: Spy.Push Errors In this example, I have tried to apply an agileFilter function onto a string attribute. This error can occur when an asset may have a string or scalar attribute exist instead of a signal attribute when a field instrument does not exist for this asset while a sibling asset would have a signal attribute and you want to apply the agileFilter. It can be difficult to determine which attributes are causing the issue. You can catalog the errors and allow spy.push to continue to push the Asset Tree without the errors and cataloging the errors. push_results_df = spy.push(metadata=build_df, workbook=workbookid, errors='catalog') The error will be cataloged in the "Push Result" column with same error shown in the example. To view all errors, you can filter the push result to see where the push result was not success. error = push_results_df[push_results_df['Push Result'] != "Success"] error
  11. Working with Asset Groups or spy.assets.Tree() has been effective. However, as your project expands, the process of adding and managing additional assets has become cumbersome. It's time to elevate your approach by transitioning to an Asset Template for building your Asset Tree. In this guide, I'll share valuable tips and tricks for creating an asset tree from an existing hierarchy. If you already have an established asset tree from another source, such as PI AF, you can leverage it as a quick starting point to build your tree in Seeq. Whether you're starting with an existing asset tree or starting from scratch, these insights will help you seamlessly integrate your asset structure into Seeq Data Lab. Additionally, I'll cover essential troubleshooting techniques, focusing on maximizing ease of use when using Asset Templates. In a future topic, I'll discuss the process of building an Asset Tree from scratch using Asset Templates. Let's dive into the best practices for optimizing your asset management workflow with Seeq Data Lab. Why Templates over Trees? The choice between using Asset Templates and spy.assets.Tree() depends on the complexity of your project and the level of customization you need. Here are some reasons why you might want to use Asset Templates instead of spy.assets.Tree(): Object-Oriented Design: Asset Templates allow you to leverage object-oriented design principles like encapsulation, inheritance, composition, and mixins. This can increase code reuse and consistency, especially in larger projects. However, the learning curve is a bit longer if you do not come from a programming background. Customization: Asset Templates provide a higher level of customization. You can define your own classes and methods, which can be useful if you need to implement complex logic or calculations. You can more easily create complex hierarchical trees similar Asset Trees from PI AF. Handling Exceptions: Manufacturing scenarios often have many exceptions. Asset Templates can accommodate these exceptions more easily than spy.assets.Tree(). Here are some reasons why you might want to use spy.assets.Tree() instead of Asset Templates: Simplicity: spy.assets.Tree() is simpler to use and understand, especially for beginners or for simple projects. If you just need to create a basic hierarchical structure without any custom attributes or methods, spy.assets.Tree() is a good choice. You can always graduate to Asset Templates later. Flat Structure: If your data is already organized in a flat structure (like a CSV file), spy.assets.Tree() can easily convert that structure into an asset tree. Less Code: With spy.assets.Tree(), you can create an asset tree with just a few lines of code. You don't need to define any classes or methods. Okay, we are settled on Asset Templates. What's first? If you have an existing asset hierarchy with the attributes you want to use in your calculations, you can start there. I'll be using the Example >> Cooling Tower 1 as my example existing Asset Tree. We'll use the asset tree path for our spy.search criteria and use the existing asset hierarchy when building our Template. The attachment includes a complete working Jupyter notebook. You can upload it to a Data Lab project and test each step as described here. Okay, let's review the code section by section. # 1. Importing Libraries from seeq import spy import pandas as pd import re from seeq.spy.assets import Asset # 2. Checking and displaying the spy version version = spy.__version__ print(version) # 3. Setting the compatibility option so that you maximize the chance that SPy # will remain compatible with your notebook/script spy.options.compatibility = 189 # 4. Disply configuration for Pandas to show all columns in DataFrame output pd.set_option('display.max_colwidth', None) In this cell, the script initializes the Seeq Python (SPy) library as well as other required libraries, checks the spy version, sets a compatibility option, and configures pandas to display DataFrame output without column width truncation. # 1. Use spy.search to search for assets using the existing Asset Tree hierarchy. metadata = spy.search( {'Path': 'Example >> Cooling Tower 1 >> Area*'}, # MODIFY: Change the search path old_asset_format=False, limit=None, ) # 2. Split the "Path" column by the ">>" delimiter and create a new column "Modified_Path." metadata["Modified_Path"] = metadata['Path'] + ' >> ' + metadata['Asset'] split_columns = metadata['Modified_Path'].str.split(' >> ', expand=True) # 3. Rename the columns of the split DataFrame to represent different hierarchy levels. split_columns.columns = [f'Level_{i+1}' for i in range(split_columns.shape[1])] # 4. Concatenate the split columns with the original DataFrame to incorporate the hierarchy levels. metadata = pd.concat([metadata, split_columns], axis=1) # 5. Required for building an asset tree. This is the parent branch/name of the asset tree. metadata['Build Path'] = 'Asset Tree Descriptive Name' # MODIFY: Set the desired parent branch name # 6. The child branch of 'Build Path' and will be named after the "Level_2" column. metadata['Build Asset'] = metadata['Level_2'] # Ex: "Cooling Tower 1" This cell constructs the metadata used in our new asset tree. The spy.search on the existing hierarchy is used to retrieve the metadata for each attribute in the asset path. The existing tree path is "Example >> Cooling Tower 1 >> Area*". This will fetch all attributes under each of the Areas (e.g., Area A - K). However, it will not retrieve any attributes that may reside in "Example >> Cooling Tower 1. The complete path for each asset is the concatenation of the "Path" column and the "Asset" column. This "Modified Path" is how we will define each level of our new Asset Tree. We are splitting the Modified Path and inserting each level as a new column that will be referenced later when building the asset tree. In steps 5 and 6, we define the first two levels of the asset tree. In this example, the new path will start with "Asset Tree Descriptive Name >> Cooling Tower 1". By utilizing the 'Level_2' column in the 'Build Path,' if we had included all cooling towers in our spy.search, the new second level in the path could be 'Cooling Tower 2,' depending on the asset's location in the existing tree." Alternatively, if you prefer a flat tree, you can specify the name of "Level 2" as a string rather than referencing the metadata column. # This is the class will be called when using Spy.Asset.Build # This is the child branch of 'Build Asset' # Ex: Asset Tree Descriptive Name >> Cooling Tower 1 >> Area A # Can you also have attributes at these levels by adding @Asset.Attribute() within the class. class AssetTreeName(Asset): # MODIFY: AssetTreeName @Asset.Component() def Asset_Component(self, metadata): # MODIFY: Unit_Component return self.build_components( template=Level_4, # This the name of the child branch class metadata=metadata, column_name='Level_3' # Metadata column name of the desired asset; Ex: Area A ) # Example Roll Up Calculation # This roll up calculation will find the evaluate the temperature if each child branch (e.g., Area A, Area B, # Area C, etc.) and create a new signal of the maximum temperature of the group @Asset.Attribute() def Cooling_Tower_Max_Temperature(self, metadata): return self.Asset_Component().pick({ ## MODIFY: Unit_Component() if changed in @Asset.Component() above 'Name': 'Temperature Unique Raw Tag' # MODIFY:'Temperature Unique Raw Tag'; this is the Friendly name of the attribute created in Level_4 }).roll_up('maximum') # MODIFY: 'maximum' In this cell, we are building our first class in our Asset Template. In the context of Asset Templates, consider a class as a general contractor responsible for constructing the current level in your asset tree. It subcontracts the responsibility for the lower level to that level's class. TIn the class, you define attributes such as signals, calculated signals, conditions, scorecard metrics, etc. or components, which are the 'child' branches of this 'parent' branch. These components are built by a different class. This first class will be called when we use spy.assets.build to create the new asset tree. @Asset.Component() indicates that we are defining a method to construct a component. The method name is Asset_Component which will construct the child branch. The parameter 'template' defines the class that will construct the child branch, while 'column_name' specifies the name of each child branch. Here we are using "Level_3" which contains the Area name (e.g., Area A). @Asset.Attribute() indicates that we are defining a method that will be an attribute. More details about creating attributes will be discussed in the next class. However, the example provided is a roll up calculation which will be a signal equal to the maximum value of each of the child branches' "Temperature Unique Raw Tag" which will be defined in the next class. When using roll-up calculations, note that the method being called is the component method responsible for constructing the child branches, not the attribute residing in the child branches. The .pick() function utilizes the friendly name of the attribute to identify the attributes in the child branches. # This is the child branch of 'AssetTreeName' class class Level_4(Asset): @Asset.Attribute() # Raw Tag (Friendly Name is the same as the method name with '_' removed) # Use this method if you are highly confident there are no multiple matches in current or child branches def Temperature_Unique_Raw_Tag(self, metadata): # MODIFY: Temperature_Unique_Raw_Tag return metadata[metadata['Name'] == 'Temperature'] @Asset.Attribute() def Temperature_Possible_Duplicates_Raw_Tag(self, metadata): # MODIFY:Temperature_Possible_Duplicates_Raw_Tag # If multiple matching metadata is found, the first match is selected using .iloc[[0]]. # This also addresses duplicate attributes in child branches. # If selecting first match is not desired, improve regex search criteria # or add additional filters to select desired attribute (e.g, # metadata[metadata['Name'].str.contains(r'(?:Temperature)$') # & metadata[metadata['Description'].str.contains(r'(?:Discharge)')] # ) filtered = metadata[metadata['Name'].str.contains(r'(?:Temperature)$')] # MODIFY: r'(?:Temperature)$') return filtered.iloc[[0]] In our example, Level_4 is the lowest level in our asset tree. All our attributes related to an asset will reside here. In our example, our raw tags (i.e., no calculations) will reside in the same branch as our derived tags (i.e., calculated), which typically reference the raw tags in the formula parameter. Each attribute is defined by a method starting with @Asset.Attribute before defining the method. When not specified, the friendly name of the attribute is the same as the method name with the underscores removed. Example: The method name is Temperature_Unique_Raw_Tag, and the friendly name is "Temperature Unique Raw Tag". We reference the metadata dataframe with a search criteria to select the proper row for the attribute. There are several methods using pandas to select the correct row. Here we are using the "Name" to find the row with "Temperature" as the value. Spy will find each "Temperature" value for each asset. This method can result in an error if there are multiple matches. The second method "Temperature_Possible_Duplicates_Raw_Tag" explains how you can avoid this error by using .iloc[[0]] to select the first match found or how you can increase the complexity of your search criteria to further filter your results. # Calculated Tag (Signal) referencing an attribute in this class @Asset.Attribute() def Temperature_Calc(self, metadata): return { 'Name': 'Signal Friendly Name', # MODIFY: 'Signal Friendly Name' 'Type': 'Signal', # This is the same formula you would use in Workbench Formula 'Formula': '$signal.agileFilter(1min)', # MODIFY: "$signal.agileFilter(1min)"; 'Formula Parameters': {'$signal': self.Temperature_Unique_Raw_Tag()}, # MODIFY: '$signal' and self.Temperature_Unique_Raw_Tag() } # Calculated Tag (Signal) not previously referenced as an attribute @Asset.Attribute() def Wet_Bulb(self, metadata): filtered = metadata[metadata['Name'].str.contains(r'^Wet Bulb$')] # MODIFY: '^Wet Bulb$' return { 'Name': 'Signal Friendly Name 2', # MODIFY: 'Signal Friendly Name 2' 'Type': 'Signal', 'Formula': '$a.agileFilter(1min)', # MODIFY: '$a.agileFilter(1min)' 'Formula Parameters': {'$a': filtered.iloc[[0]]}, # MODIFY: '$a' } # Calculated Tag (Condition) @Asset.Attribute() def Temperature_Cond(self, metadata): return { 'Name': 'Condition Friendly Name', # MODIFY: 'Condition Friendly Name' 'Type': 'Condition', 'Formula': '$b > 90', # MODIFY: '$b > 90' 'Formula Parameters': {'$b': self.Temperature_Possible_Duplicates_Raw_Tag()}, # MODIFY: '$b' & self.Temperature_Possible_Duplicates_Raw_Tag() } # Scorecard Metric # See Asset Templates documentation for more examples of Scorecard Metrics @Asset.Attribute() def Temperature_Statistic_KPI(self, metadata): return { 'Type': 'Metric', 'Measured Item': self.Temperature_Unique_Raw_Tag(), # MODIFY: self.Temperature_Unique_Raw_Tag() 'Statistic': 'Average' # MODIFY: 'Average' } This is a continuation of our Level_4 class. Here we are creating derived attributes which use a formula or metric and reference other attributes in most cases. There are several different example attributes for calculated signals, conditions, and scorecard metrics. You can specify the friendly name in the "Name" parameter as an alternative method to using the name of the method. The value of the "Formula" is the exact same formula you use in Workbench. "Formula Parameters" is where you define your variables in your formula. The value of the "variable" key (i.e., $signal) is a method call to the attribute to be used in the formula (i.e., self.Temperature_Unique_Raw_Tag()). If you wish to specify a signal that has not been defined as an attribute, you can specify the value of the referenced variable using the same search criteria when specifying a new attribute. It is important to remember to change the "Type" when creating a condition or a scorecard metric, as this will cause an error if not properly specified. # Use this method if you want a different friendly name rather than the regex search criteria @Asset.Attribute() def Friendly_Name_Example(self, metadata): return self.select_metadata(metadata, name_contains = r'^Compressor Power$', friendly_name ='Power') #MODIFY: name_contains = r'Compressor Power', friendly_name = 'Power' # Use this method if you want the friendly name to be the same as regex search criteria # without the regex formatting @Asset.Attribute() def No_Friendly_Name_Example(self, metadata): return self.select_metadata(metadata, r'^Compressor Power$', None) # Example of a string attribute being passed through @Asset.Attribute() def String_Example(self, metadata): return self.select_metadata(metadata, r'Compressor Stage$', None) # Example of an attribute not found @Asset.Attribute() def No_Attribute_Exists(self, metadata): return self.select_metadata(metadata, r'Non-existing Attribute', None) # 1. Method below matches the metadata, perform attribute type detection, and applies proper formula def select_metadata(self, metadata, name_contains, friendly_name): filtered = metadata[metadata['Name'].str.contains(name_contains)] if not filtered.empty: # checks if metadata is a signal vs scalar and selects the first signal type if filtered['Type'].str.contains('signal', case=False).any() : signal_check = filtered[filtered['Type'].str.contains('signal', case=False) & ~filtered['Value Unit Of Measure'].str.contains('string', case=False)] filtered_signal = signal_check if len(signal_check) > 0 else filtered.iloc[[0]] selected_metadata = filtered_signal.iloc[[0]] else: selected_metadata = filtered.iloc[[0]] return self.determine_attribute_type(selected_metadata, name_contains, friendly_name) else: # This returns a signal with no values rather than not adding an attribute when # an attribute cannot be found. If this is not desired, simply comment out the # else statement. Having a signal is recommended to assist in asset swapping. return { 'Name' : friendly_name if friendly_name is not None else re.sub( r'\^|\(|\$|\(\?:|\)$|\)', '', name_contains), 'Type': 'Signal', 'Formula': 'Scalar.Invalid.toSignal()', } # 2. Determine if an attribute is a signal or scalar type def determine_attribute_type(self, metadata, name_contains, friendly_name): if metadata['Type'].str.contains('signal', case=False).any(): return self.generic_metadata_function(metadata, name_contains, 'Signal', friendly_name) elif metadata['Type'].str.contains('scalar', case=False).any(): return self.generic_metadata_function(metadata, name_contains, 'Scalar', friendly_name) return None # 3. Creates the metadata to create the attribute def generic_metadata_function(self, metadata, name_contains, formula_type, friendly_name): if friendly_name is None: # Regular expression pattern to remove caret (^), first "(", dollar ($), # non-capturing groups (?:), and trailing ")" pattern = r'\^|\(|\$|\(\?:|\)$|\)' friendly_name = re.sub(pattern, '', name_contains) # Metadata for signals includes strings. Required to separate strings to perform a # formula on non-string signals. Check if the signal is a "string" based on 'Value Unit Of Measure' if formula_type == 'Signal' and metadata['Value Unit Of Measure'].str.contains( 'string', case=False).any(): formula_type = 'String' # If signal, perform a calculation. If string or scalar, pass only the variable in formula. if formula_type == 'Signal': formula = '$signal.validValues()' # MODIFY: '$signal.validValues()' elif formula_type == 'String': formula = '$signal' else: formula = '$scalar' return { 'Name' : friendly_name, 'Type': formula_type, 'Formula': formula, 'Formula Parameters': ( {'$signal': metadata} if formula_type in ['Signal', 'String'] else {'$scalar': metadata}) } This is a continuation of the Level_4 class. This section is more of a trick than a tip. A common request is how to bulk apply a formula to all signals as an initial data cleaning step. This can be challenging if your existing asset tree contains signals, scalars, and/or strings with the same friendly name in the parent and/or child branches. I've seen this happen when an instrument does not exist for a particular asset. The attribute still exists in the asset tree but contains no data and is either a scalar or string. This method allows users to quickly perform the same calculations on all raw data signals. It will check for string, scalar, and signal types to determine if a calculation is required. If there are multiple matches, it will preferentially select a signal over a string or scalar. It will perform the appropriate calculation on the tag based on the type or return a blank signal if the attribute is not found. The blank signal is a necessary evil for asset swapping if the attribute is used in a calculation for other attributes. The formula should be adjusted to handle when there are no valid values in the attribute. @Asset.Attribute() def Friendly_Name_Example(self, metadata): return self.select_metadata(metadata, name_contains = r'^Compressor Power$', friendly_name ='Power') This attribute method has been modified to make a method call to select_metadata. It passes the metadata, name_contains, which is the search criteria for finding the name value in the metadata "Name" column, and a friendly name. In this example, the attribute will be named "Power". @Asset.Attribute() def No_Friendly_Name_Example(self, metadata): return self.select_metadata(metadata, r'^Compressor Power$', None) If the friendly name does not need to be different from the name_contains parameter without the regex formatting (i.e., special characters), it will name the attribute the search criteria name minus special characters. In the example above the attribute will be named "Compressor Power". This naming method is fairly simple and is not dynamic enough to insert result of the regex search. In those cases, the friendly_name should be specified. The subsequent method call to determine_attribute_type determines the attribute type (e.g., signal, string, scalar). If multiple matches are found, it will select the first signal type over a scalar or string. If attribute is not found, it creates an empty attribute. The final method call to generic_metadata_function determines the friendly name to be used and selects the correct formula for the proper attribute type. As I said earlier, this is more of a trick that address most common issues I have faced when dealing with complex asset trees and applying a common formula in bulk. This can also be applied even if you don't want to apply a formula to all signals. In that case, you would simply modify the formula to be "$signal" instead of "$signal.validValues()". You can still preferentially select signals types over string or scalar types. However, if referencing one of these attributes in a calculated attribute later, you will still have to use the attribute method name (e.g., self.Friendly_Name_Example() when referencing the 'Power' attribute in the formula parameter). build_df = spy.assets.build(AssetTreeName,metadata) # Check for success for each tag for an asset build_df.head(15) After creating our asset structure and attributes, you can run this cell to build our asset tree. I recommend visually checking for success in the 'Build Result' column of the dataframe, looking for 'Success' in each attribute for a single asset. workbookid = "WORKBOOK_ID" # MODIFY: "WORKBOOK_ID" # If you want the tree to be available globally use None as the workbookid push_results_df = spy.push(metadata=build_df, workbook=workbookid, errors='catalog') # Check for success at Push Result push_results_df.head(15) Once you are satisfied with your results or have resolved any errors, you can push your asset tree. If you wish to push your tree globally, change the value of the 'workbook' argument in spy.push to be set to None. I recommend doing this only after addressing all the issues in a workbook, which can serve as your sandbox environment. errors = push_results_df[push_results_df['Push Result'] != "Success"] errors The final step I perform is to check if any of my attributes were not successfully pushed. If there were no issues, the error dataframe should contain no rows. Well, if you have made it to the end, congrats! If you skipped to the end, that's fine too. In this guide, we delved into the world of Asset Templates and their role in streamlining the creation and management of asset trees within Seeq Data Lab. By leveraging object-oriented design principles, encapsulation, and inheritance, Asset Templates offer a powerful way to build complex hierarchical structures. Whether you're dealing with an existing asset tree or starting from scratch, the insights shared here aim to maximize the efficiency of your workflow. Download the attached Jupyter notebook and experiment with the code by modifying or adding new attributes or applying it to a different asset tree. Remember, the best way to grasp these concepts is to dive into the provided Jupyter notebook, make modifications, and witness the results firsthand. In the comments below, I will add some screenshots of common errors I come across when using spy.assets.build and what you should do to troubleshoot the errors. Tips Tricks for Existing Asset Trees.ipynb
×
×
  • Create New...