Jump to content

Joe Reckamp

Seeq Team
  • Posts

    128
  • Joined

  • Last visited

  • Days Won

    36

Everything posted by Joe Reckamp

  1. Capsules can have properties or information attached to the event. By default, all capsules contain information on time context such as the capsule’s start time, end time, and duration. However, one can assign additional capsule properties based on other signals’ values during the capsules. Datasources can also bring in conditions with capsule properties already added. This guide will walk through some common workflows to visualize, add, and work with capsule properties. How can I visualize capsule properties? Capsule Properties can be added to the Capsules Pane in the bottom right hand corner of Seeq Workbench with the black grid icon. Any capsule properties beyond start, end, duration, and similarity that are created with the formulas that follow or come in automatically through the datasource connection can be found by clicking the “Add Column” option and selecting the desired property in the modal. In the capsule pane, you can filter on capsule properties by clicking on the three dots next to a column. In Trend View, you can add Capsule Property labels inside the capsules by selecting the property from the Labels drop down at the top of the trend. In Capsule Time, the signals can be colored by Capsule Properties by turning on the coloring to rainbow or gradient. The selection for which property is performing the coloring is done by sorting by the capsule property in the Capsule Pane in the bottom right corner. Therefore, if Batch ID is sorted by as selected below, the legend shown on the chart will show the Batch ID values. When working with Condition Table, you can add capsule properties as columns or as headers How do I create a capsule for every (unique) value of a signal? The Condition with Properties tool allows you take one or more step signals and turn them into a condition with a capsule per value change. The signal values will be added as properties on the capsules. For instance, if we have a BatchID and an Operation signal, we can use this tool to generate a capsule per Operation. In formula this would be done using the toCondition() operator: $batchID.toCondition('Batch ID') Where Batch ID is the name of the resulting property on the condition. How do I assign a capsule property? Option 1: Assigning a constant value to all capsules within a condition $condition.setProperty('Property Name', 'Property Value') Note that it is important to know whether you would like the property stored as a string or numeric value. If the desired property value is a string, make sure that the ‘Property Value’ is in single quotes to represent a string like the above formula. If the desired value is numeric, you should not use the single quotes. Option 2: Assigning a property based on another signal value during the capsule For these operations, you can also use the setProperty operator. For example, you may want the first value of a signal within a capsule or the average value of a signal during the capsule. Below are some examples of options you have and how you would set these values to capsule properties. $condition.setProperty('Property Name', $signal, aggregationMethod()) A full list of aggregation methods can be found under the Signal Value Statistics and Condition Value Statistics pages in the formula documentation, but common aggregation methods include: startValue() endValue() max() average() count() totalDuration() Option 3: Moving properties between conditions (e.g. parent to child conditions) In batch processing, there is often a parent/child relationship of conditions in an S88 (or ISA-88) tree hierarchy where the batch is made up of smaller operations, which is then made up of smaller phases. Some events databases may only set properties on particular capsules within that hierarchy, but you may want to move the properties to higher or lower levels of that hierarchy. You can accomplish this using mergeProperties() $conditionWithoutProperty.mergeProperties($conditionWithProperty) How do I filter a condition by capsule properties? Conditions are filtered by capsule properties using the keep() operator. Some examples of this are listed below: Option 1: Keep exact match to property $condition.keep('Property Name', isEqualTo('Property Value')) Note that it is important to know whether the property is stored as a string or numeric value. If the property value is a string, make sure that the ‘Property Value’ is in single quotes to represent a string like the above formula. If the value is numeric, you should not use the single quotes. Option 2: Keep regular expression string match $condition.keep('Property Name', isMatch('B*')) $condition.keep('Property Name', isNotMatch('B*')) You can specify to keep either matches or not matches to partial string signals. In the above formulas, I’m specifying to either keep all capsules where the capsule property starts with a B or in the second equation, the ones that do not start with a B. If you need additional information on regular expressions, please see our Knowledge Base article. Option 3: Other keep operators for numeric properties Using the same format of Option 1 above, you can replace the isEqualTo operator with any of the following operators for comparison functions on numeric properties: isGreaterThan isGreaterThanOrEqualTo isLessThan isLessThanOrEqualTo isBetween isNotBetween isNotEqualTo Option 4: Keep capsules where capsule property exists $condition.keep('Property Name', isValid()) In this case, any capsules that have a value for the property specified will be retained, but all capsules without a value for the specified property will be removed from the condition. How do I turn a capsule property into a signal? A capsule property can be turned into a signal by using the toSignal() operator. The properties can be placed at either the start timestamp of the capsule (startKey), the end timestamp of the capsule (endKey), or the entire duration of the capsule (durationKey). For most use cases the default of durationKey is the most appropriate. $condition.toSignal('Property Name') // will default to durationKey $condition.toSignal('Property Name', startKey()) $condition.toSignal('Property Name', endKey()) Using startKey or endKey will result in a discrete signal. If you would like to turn the discrete signal into a continuous signal connecting the data points, you can do so by adding a toStep or toLinear operator at the end to either add step or linear interpolation to the signal. Inside the parentheses for the interpolation operators, you will need to add a maximum interpolation time that represent the maximum time distance between points that you would want to interpolate. For example, a desired step interpolation of capsules may look like the following formula: $condition.toSignal('Batch ID', startKey()).toStep(40h) How do I rename capsule properties? Properties can be swapped to new names by using the renameProperty operator: $condition.renameProperty('Current Property Name', 'New Property Name') A complex example of using capsule properties: What if you had upstream and downstream processes where the Batch ID (or other property) could link the data between an upstream and downstream capsule that do not touch and are not in a specific order? In this case, you would want to be able to search a particular range of time for a matching property value and to transfer another property between the capsules. This can be explained with the following equation: $upstreamCondition.move(0, 7d).mergeProperties($downstreamCondition, 'Batch ID').move(0, -7d) Let's walk through what this is doing step by step. First, it's important to start with the condition that you want to add the property to, in this case the upstream condition. Then we move the condition by a certain amount to be able to find the matching capsule value. In this case, we are moving just the end of each capsule 7 days into the future to search for a matching property value and then at the end of the formula, we move the end of the capsule back 7 days to return the capsule to its original state. After moving the capsules, we merge properties with the downstream condition, only bringing properties from overlapping downstream capsules with the same Batch ID as the upstream condition. Using capsule properties in histogram You can create bins using capsule properties in the histogram tool by selecting the 'Condition' aggregation type. The following example creates a Histogram based upon the Value property in the toCondition() condition. The output is a Histogram with a count of the number of capsules with a Value equal to each of the four stages of operation: Creating Capsule Properties Reference Video: Conditions and capsules in Seeq are key to focusing #timeseries data analytics on specific time periods of interest. In this video, we explore how to create capsule properties to add additional context to the data. Using Capsule Properties Reference Video: Conditions and capsules in Seeq are key to focusing analytics on specific time periods of interest. In this video, we explore how to use capsule properties, the data attached to an event, to supply further insights into the data. Content Verified DEC2023
  2. Hi kward, You can perform calculations across time by using the delay operator in Formula (or if you are on version 49+, the move operator). For example, if I wanted to calculate the current value minus 4 years ago, you can do the following Formula: $signal-$signal.delay(4y) This is essentially saying take my signal and subtract it from the same signal, just moved forward 4 years in time. By doing this, you align the current time and the time exactly 4 years ago to the same timestamp so that it can be subtracted. Note: Again, just for any people on version 49+, the .delay() operator would be switched to .move(). Note that this will do the calculation for every point in time. If you would like to do this based on daily, weekly, or monthly averages, then you should use Signal from Condition to calculate the average you'd like and use that as the $signal input into the Formula above.
  3. Hi Dominic, In order to specify both start and end time, you have to specify the exact time rather than just the day as the day or year option creates both a start and end time (for example, '2019' created both a start of Jan 1, 2019 at 12:00 AM and an end of Jan 1, 2020 at 12:00 AM). Therefore, your function would need to be formatted like this: 20$.tosignal().splice(10$.tosignal(), condition(100d, capsule('2019-02-01T00:00Z','2019-05-12T00:00Z')))
  4. Hi Dominic, You can use the splice function in Formula to complete this request. I'm assuming your cost changes each year so you may want to splice in a different cost for each year. For example, you can use the following Formula to say take the current cost of $20 and splice in $10 cost for all of 2019 so that on January 1st, it steps up to the new cost: 20$.tosignal().splice(10$.tosignal(), condition(1y, capsule('2019'))) Let me also break this Formula down a bit so that you understand it better. In the first part, we are saying take a value of $20 (as a signal) as the default result, which means that anytime before or after 2019 (in this case), the value would equal $20. However, when the capsule is present in the condition in the splice condition is met (in this case it's a capsule for all of 2019), the $10 signal will be spliced instead of the $20 signal. I also want to note that the '1y' argument in the condition is the maximum duration. If you wanted to expand the capsule to be longer than 1 year, you would also need to edit that value. If you want to add additional years at the $10 value, you can simply add them as more capsules in the condition argument. For example, adding 2018: 20$.tosignal().splice(10$.tosignal(), condition(1y, capsule('2019'), capsule('2018'))) If you want to add different values instead (let's say $15 for 2018), you could use the following modification: 20$.tosignal().splice(10$.tosignal(), condition(1y, capsule('2019'))).splice(15$.tosignal(), condition(1y, capsule('2018')))
  5. Hi aeina, You probably have something that looks like this with regards to the capsules you have already found: In order to do what you want, you first need to make capsules representing the time period you want to aggregate across. You can do this using Periodic Condition. For example if I wanted to compare years (2018 vs. 2019), I would make a yearly capsule. If I wanted to compare month to month, I could create a monthly capsule: Once you have that made, you can then use Signal from Condition to calculate the Total Duration of the original capsules during the bounding condition of the Monthly or Yearly capsule you created: This will give you the view like this: Alternately, you could use Scorecard Metric to create a tabular view of the same data:
  6. Hi Ario, The Seeq version of Excel's IF function is called splice. In order to do what you would like to do, you would want to do 2 steps: 1. Create a Value Search for when $t1 = 1 2. Create a Formula that splices either a value of 0 or the sum formula depending on whether $t1 is 1 or not. The formula would look like: 0.tosignal().splice($t2*$t3+$t4*$t5,$ConditionfromStep1) This effectively says make the value 0 when a capsule for $t=1 is not present, but when a capsule becomes present, make the value of the signal the sum of $t2*$t3+$t4*$t5.
  7. Hi Bharat, I understand what you are trying to do. For static limits, you can use the custom columns to create your limit metrics and color code them (see example below), but it is not currently possible to automatically add those limits. However, we can file a feature request for that.
  8. Hi Bharat, I'm not sure if this is exactly what you are asking about, but you are able to set the thresholds and limits as their own metrics (see below screenshot) and label with the limit type as long as those thresholds are a signal. If the value is a scalar (such as 95), you can turn it into a signal by using Formula with "$scalar.tosignal()". You can see in this case, I also color coded it the color of the threshold I gave it by just setting a threshold of zero so that it is always colored the way I would like. If that is not answering the question, can you please clarify some more as to what you are looking for?
  9. Hi Leonardw, You will see in the OData Export Knowledge Base article (https://support.seeq.com/space/KB/112868662/OData%20Export) that there is an option for an auto-updating OData feed. This feature requires a premium license so if you do not have that feature available, please contact your Seeq Sales Executive for more details on the premium license. If you are unsure of your Seeq Sales Executive, please private message me what company you work for and I can point you to the correct person.
  10. 5. Use a Sankey visualization to view the relationships between batches. In order to use the Sankey visualization, first an additional column will need to be added to describe the relative width of each of the combinations. In this example, we are just going to add a column that equally weights each of the visualizations. However, if input weights or mass fractions were available for each combination, that could be used in this column instead. Finally, call the Sankey visualization code taken from an example website and modify functions slightly to match DataFrame set up and desired colors. FinalDf['Count']=1 def genSankey(df,cat_cols=[],value_cols='',title='Sankey Diagram'): # maximum of 6 value cols -> 6 colors colorPalette = ['#FFD43B','#306998','#23B9F2','#089474','#646464','#F23E29'] labelList = [] colorNumList = [] for catCol in cat_cols: labelListTemp = list(set(df[catCol].values)) colorNumList.append(len(labelListTemp)) labelList = labelList + labelListTemp # remove duplicates from labelList labelList = list(dict.fromkeys(labelList)) # define colors based on number of levels colorList = [] for idx, colorNum in enumerate(colorNumList): colorList = colorList + [colorPalette[idx]]*colorNum # transform df into a source-target pair for i in range(len(cat_cols)-1): if i==0: sourceTargetDf = df[[cat_cols[i],cat_cols[i+1],value_cols]] sourceTargetDf.columns = ['source','target','count'] else: tempDf = df[[cat_cols[i],cat_cols[i+1],value_cols]] tempDf.columns = ['source','target','count'] sourceTargetDf = pd.concat([sourceTargetDf,tempDf]) sourceTargetDf = sourceTargetDf.groupby(['source','target']).agg({'count':'sum'}).reset_index() # add index for source-target pair sourceTargetDf['sourceID'] = sourceTargetDf['source'].apply(lambda x: labelList.index(x)) sourceTargetDf['targetID'] = sourceTargetDf['target'].apply(lambda x: labelList.index(x)) # creating the sankey diagram data = dict( type='sankey', node = dict( pad = 15, thickness = 20, line = dict( color = "black", width = 0.5 ), label = labelList, color = colorList ), link = dict( source = sourceTargetDf['sourceID'], target = sourceTargetDf['targetID'], value = sourceTargetDf['count'] ) ) layout = dict( title = title, font = dict( size = 10 ) ) fig = dict(data=[data], layout=layout) return fig fig = genSankey(FinalDf,cat_cols=['Step 1 Input Batch ID','Step 2 Input Batch ID','Step 3 Input Batch ID','Step 4 Input Batch ID','Step 5 Input Batch ID','Step 6 Batch ID'],value_cols='Count',title='Batch Genealogy') plotly.offline.plot(fig, validate=False) In this visualization, each different colored column represents a step in the process (yellow = Step 1, dark blue = Step 2, etc.) with the batch ID of the step denoted in the text next to it. The gray connections represent the various combinations of where that Batch ID was used in the subsequent step. With the above code, the Sankey visualization will open in a new tab and using Plotly, the visualization will be interactive. Therefore, when you highlight a specific batch, it will highlight its dependencies so you can quickly see which batches are related to that particular batch highlighted.
  11. In batch processes, genealogy of lots of batch IDs is important to understand. When a deviation occurs in a raw material or batch, the use of that material in subsequent steps of the process needs to be quickly identified. In the case of pharmaceuticals, batches that use that product may need to be pulled from the shelves immediately and not used by consumers. Batch Genealogy starts with having data in a format where the input material lot numbers or Batch IDs are tracked for each batch. In Seeq, these input material lot numbers or Batch IDs are tracked as properties attached to each Batch capsule. The properties can either be set up from the datasource connector (such as bringing in OSIsoft PI Event Frames or information from a SQL database) or they can be built using transforms in Seeq Formula. Once the data is set up properly, visualization can be achieved with Seeq Data Lab. This article will describe one approach to Batch Genealogy visualization in Seeq Data Lab. In this particular scenario, we are looking at 6 distinct batch chemical transformations to get to the final product and we are tracking the key raw material as it goes through the process. 1. First, use spy.search to find the batches of interest: results = spy.search({ "Name": "Step * Batches" },workbook='3FC99BE8-F721-48E1-ACC3-18751ADA549F') # Print the output to the Jupyter page results 2. Next, use spy.pull to retrieve the records for a specific time range of interest: step2_batches = results.loc[results['Name'].isin(['Step 2 Batches'])] step2_data = spy.pull(step2_batches, start='2019-01-01', end='2019-07-01', header='Name') step2_data 3. In the previous step, you can see that some batches have multiple input Batch IDs. In order to track all combinations of the input batch IDs and output batch IDs, use pandas DataFrame manipulations to get a new DataFrame with just the Input Batch ID and Output Batch ID combinations. step2_1 = step2_data[['Step 1 Input Batch ID (1)', 'Batch ID']].rename(columns={'Step 1 Input Batch ID (1)': 'Step 1 Input Batch ID'}) step2_2 = step2_data[['Step 1 Input Batch ID (2)', 'Batch ID']].rename(columns={'Step 1 Input Batch ID (2)': 'Step 1 Input Batch ID'}) step2 = pd.concat([step2_1,step2_2]).rename(columns={'Batch ID': 'Step 2 Batch ID'}).set_index('Step 2 Batch ID') step2 = step2.loc[(step2!='0.0').any(1)] step2 You may notice that instead of 6 rows in the previous DataFrame, we now have 11 rows (5 batches with 2 input batch IDs + 1 batch with 1 input batch ID = 11 total combinations). 4. Repeat the above steps for each additional step in the process, creating a final DataFrame with Step 1 through Step 6 batches of all combinations of input Batch IDs from prior steps. step5_6 = pd.merge(step6.reset_index(),step5.reset_index().rename(columns={'Step 5 Batch ID': 'Step 5 Input Batch ID'})) step4_5_6 = pd.merge(step5_6,step4.reset_index().rename(columns={'Step 4 Batch ID': 'Step 4 Input Batch ID'})) step3_4_5_6 = pd.merge(step4_5_6,step3.reset_index().rename(columns={'Step 3 Batch ID': 'Step 3 Input Batch ID'})) FinalDf = pd.merge(step3_4_5_6,step2.reset_index().rename(columns={'Step 2 Batch ID': 'Step 2 Input Batch ID'})) FinalDf
  12. Various parts of the world display date and time stamps differently. Often times, we get requests for changing the order of month and day in the timestamp string or to display the date as a Scorecard metric in a specific format. This can be done using the replace() operator in Formula. For example, let's say we wanted to pull the start time for each capsule in a condition and display it as mm/dd/yyyy hh:mm format: $condition.transformToSamples($cap -> Sample($cap.getStart(),$cap.getProperty('Start')), 1d) .replace('/(?<year>....)-(?<month>..)-(?<day>..)T(?<hour>..):(?<minute>..):(?<sec>..)(?<dec>.*)Z/' , '${month}/${day}/${year} ${hour}:${minute}') This takes the original timestamp (for example: '2019-11-13T17:04:13.7220714157Z') and parses it into the year, month, day, hour, minute, second, and decimal to be able to set up any format desired. The various parts of the string can then be called in the second half of the replace to get the desired format as shown above with ${month}/${day}/${year} ${hour}:${minute}. From there, you can either view this data in the trend or use Scorecard Metric to display the Value at Start in a condition based metric. If the end time is desired instead of the start, the only changes needed would be to (1) switch the .getStart operator to .getEnd, and (2) switch the .getProperty('Start') to .getProperty('End'). Note: The '1d' at the end of the 2nd line of the formula represents the maximum interpolation for the data, which is important if you want to view this as a string signal. This value may need to be increased depending on the prevalence of the capsules in the condition.
  13. How can i calculate the mean kinetic temperature (MKT)? In many industries (pharmaceuticals, food and beverage, etc.), mean kinetic temperature (MKT) is used to measure the temperature fluctuations of a material during storage and shipment. Mean Kinetic Temperature is a non-linear weighted average temperature that is set up to provide an impact on product stability. In general, product stability follows an exponential trend with temperature as it is inherently a decomposition reaction of the desired product. Therefore, the mean kinetic temperature takes into account the exponential reaction rate to determine the average temperature weighted by the kinetics of the reaction over time. The formula for mean kinetic temperature is: Where: is the mean kinetic temperature in Kelvin is the activation energy (in kJ mol−1) is the gas constant (in J mol−1 K−1) to are the temperatures at each of the sample points in kelvins to are time intervals at each of the sample points
  14. Question: I've already got a Seeq Workbench with a mix of raw signals and calculated items for my process. Is there a way to grab all signals and conditions on the display rather than having to find all of those through a spy.search()? Answer: Yes, you can absolutely specify the workbook ID and the worksheet number to grab the display items from the Seeq Workbench. Here's an example of how to do that: The first step is to pull the desired workbook by specifying the Workbook ID: desired_workbook = spy.workbooks.pull(spy.workbooks.search({ 'ID': '741F06AE-62D6-4729-A4C3-8C9CC701A2A1' }),include_referenced_workbooks=False) If you are not aware, the Workbook ID can be found in the URL by finding the identifier following the .../workbook/... part of the URL. (e.g. https://explore.seeq.com/workbook/741F06AE-62D6-4729-A4C3-8C9CC701A2A1/worksheet/DFAC6933-A68F-4EEB-8C57-C34956F3F238). In this case, I added an extra function to not include referenced workbooks so that we only get the workbook with that specific ID. Once you have the desired workbook, you will want to grab the index 0 of the desired_workbook since there should only be one workbook with that ID. You will then want to specify which worksheet you want to grab the display items from. In order to see your options for the worksheet index, run the following command: desired_workbook[0].worksheets This command should return a list of the possible worksheets within the Workbook ID you specified. For example, an output might look like this: [Worksheet "1" (EDAA0608-29EA-4EA6-96FA-B6A59D8AE003), Worksheet "From Data Lab" (34AC07F9-F2FF-4C9E-A923-B636D6642B32)] Depending on which worksheet in the list you want to grab the display items from, you will then specify that index. Please note that the indexes start at 0, not 1 so the first worksheet will be index 0. Therefore, if I wanted the first worksheet in the list, I can specify that I want to know the display items as: displayed_items = desired_workbook[0].worksheets[0].display_items If you show what the item "displayed_items" looks like, you should get a Pandas DataFrame of the Workbench items as if you had done a spy.search for all of them. For example: You can then use displayed_items as your DataFrame to perform the spy.pull() command to grab the data from a specific time range.
  15. Tangential Flow Filtration Resistance in Seeq Many pharmaceutical and beverage processes involve a filtration process where a common type of filtration is tangential flow filtration. In biopharmaceutical processing, this is commonly found in a perfusion bioreactor and in the ultrafiltration/diafiltration (UF/DF) concentrating step. Beverage processes also typically involve a UF/DF type process to remove potential contaminants or further refine the concentration of the desired beverage. A typical tangential flow filtration set up looks like the picture below where you have an inlet stream and two outlet streams: the retentate, which did not pass through the filter and is returned to bulk inlet, and the permeate, which is typically the product stream that has been filtered. The goal of the process is to concentrate the product while removing large contaminants that may be present by filtering them out. During the filtration process, particles can build up on the filter membrane, causing additional resistance that reduces the effectiveness of the filter and slows down the filtration process. Therefore, it is imperative to effectively clean and sanitize the filter membrane between each batch and to monitor the membrane resistance over time to determine whether the unit operation is still effective. One method for monitoring this is through filter resistance calculations. In order to calculate the filter resistance, the following signals are required: · Feed Inlet Pressure · Retentate Pressure · Permeate Pressure · Permeate Flow Rate If the permeate flow rate is not present, it can be calculated, by way of the Conservation of Mass using the feed and retentate flow rates, in Seeq through the Formula tool: $FeedFlowRate = $RetentateFlowRate + $PermeateFlowRate The first step in solving for the resistance is to calculate a variable called the Transmembrane Pressure (TMP). This is an average pressure differential, or driving force, across the filter and can be calculated by the following formula in Seeq: ($FeedInletPressure + $RetentatePressure) / 2 - $PermeatePressure Membrane flux (J) is the amount of permeate per unit area of the filter. This can be calculated in Seeq Formula as well: $PermeateFlowRate / MembraneArea where MembraneArea is a value input by the user. Membrane flux (J) and TMP are related by the Darcy Equation, which is: J = TMP / (μ * R) Therefore, this equation can be rearranged to calculate the resistance (R), which includes both the inherent membrane resistance and the added resistance due to fouling. The resistance can be calculated in Seeq Formula: $TMP / ($MembraneFlux * Viscosity) where Viscosity is a value input by the user. If the viscosity value is unknown, the value can simply be removed from the equation to be grouped with resistance since it is a constant and will not impact the analysis of monitoring the change in resistance over time. This resistance value should be evaluated over time or multiple batches to determine whether there is any fouling occurring or inadequate cleaning or sanitization between batches. It is recommended to use the Boundaries tool in Seeq to set limits based on historical data sets. Lower membrane resistance than historical values may represent breakthrough in the filter due to quality defects in the filter membrane. Higher membrane resistance may represent fouling of the membrane over time and result in a loss of yield if additional filtration time is not included. This higher membrane resistance may signify that additional cleaning or membrane replacement is required. These deviations from the expected resistance values can be flagged using the Deviation Search tool within Seeq.
  16. Chromatography Transition Analysis in Seeq Many biopharmaceutical companies use Transition Analysis to monitor column integrity. Transition Analysis works by using a step change in the input conductivity signal and tracking the conductivity at the outlet of the column. The output conductivity transition can be analyzed via moment analysis to calculate the height of a theoretical plate (HETP) and the Asymmetry factor as described below. Step 1: Load Data and Find Transition Periods In order to perform this analysis in Seeq, we start by loading outlet conductivity and flow rate data for the column: Note: Depending on the density of the conductivity data, many companies find that some filtering of the data needs to be performed to get consistent results when performing the above differential equations. The agilefilter operator in Formula can be helpful to perform this filtering if needed: $cond.agileFilter(0.5min) Once the data is loaded, the next step is to find the transition periods. The transition periods can be found using changes in the signal such as a delta or derivative with Value Searches have also been applied. Combine the periods where the derivative is positive and the flow is low to identify the transitions. Alternative methods using the Profile Search tool can also be applied. Step 2: Calculate HETP As the derivatives are a function of volume instead of time, the next step is to calculate the volume using the following formula: Volume = $flow.integral($Trans) The dC/dV function used in the moment formulas can then be calculated: dCdV = runningDelta($Cond) / runningDelta($vol) Using that function, the moments (M0 through M2) can be calculated: M0 = ($dCdV*runningDelta($vol)).aggregate(sum(), $Trans, middleKey()) M1 = (($vol*$dCdV)*runningDelta($vol)).aggregate(sum(), $Trans, middleKey()) M2 = (($dCdV*($vol^2))*runningDelta($vol)).aggregate(sum(), $Trans, middleKey()) The moments are then used to calculate the variance: Variance = ($M2/$M0) - ($M1/$M0)^2 Finally, the HETP can be calculated: HETP = ((columnlength*$variance)/($M1/$M0)^2) In this case, the column length value needs to be inserted in the units desired for HETP (e.g. 52mm). The final result should look like the following screenshot: Alternatively, all of the calculations can be performed in a single Formula in Seeq as shown in the code below: $vol = $flow.integral($Trans) $dCdV = runningDelta($cond) / runningDelta($vol) $M0 = ($dCdV*runningDelta($vol)).aggregate(sum(), $Trans, middleKey()) $VdCdV = $vol*$dCdV $M1 = ($VdCdV*runningDelta($vol)).aggregate(sum(), $Trans, middleKey()) $V2dCdV = $dCdV*$vol^2 $M2 = ($V2dCdV*runningDelta($vol)).aggregate(sum(), $Trans, middleKey()) $variance = ($M2/$M0) - ($M1/$M0)^2 (52mm*$variance)/(($M1/$M0)^2) //where 52mm is the column length, L Step 3: Calculate Asymmetry Asymmetry is calculated by splitting the dC/dV peak by its max value into a right and left side and comparing the volume change over those sides. This section assumes you have done the calculations to get volume and dC/dV calculated already as performed for HETP in Step 2 above. The first step for Asymmetry is to determine a minimum threshold for dC/dV to begin and end the peaks. This is often done by calculating a percentage of the difference between the maximum and minimum part of the transition period (e.g. 10%): $min = $dCdV.aggregate(minValue(), $Trans, durationKey()) $max = $dCdV.aggregate(maxValue(), $Trans, durationKey()) $min + 0.1*($max - $min) The Deviation Search tool can then be used to identify the time when dC/dV is greater than the 10% value obtained above. Next, the maximum point of the dC/dV peaks can be determined by calculating the derivative of dC/dV in the Formula tool: $dCdV.derivative() The derivative can then be searched for positive values (greater than 0) with the Value Search tool to identify the increasing part of the dC/dV curve. Finally, a Composite Condition intersecting the positive dC/dV derivative and the transition values above 10% of the curve will result in the identification of the left side of the dC/dV curve: The right side of the dC/dV curve can then be determined using Composite Condition with the A minus B operator to subtract the positive dC/dV derivative from the transition above 10%: The change in volume can then be calculated by aggregating the delta in volume over each side of the peak using Formula: $Vol.aggregate(delta(), $LeftSide, middleKey()).aggregate(maxValue(), $Trans, middleKey()) Finally, the Asymmetry ratio can be calculated by dividing the volume change of the right side of the peak divided by the volume change of the left side of the peak. $VolRightSide/$VolLeftSide The final view should look similar to the following: Similar to HETP, all of the above formulas for Asymmetry may be calculated in a single formula with the code below: $vol = $flow.integral($Trans) $dCdV = (runningDelta($cond) / runningDelta($vol)).agileFilter(4sec) //calculate 10%ile of dCdV $min = $dCdV.aggregate(minValue(), $Trans, durationKey()) $max = $dCdV.aggregate(maxValue(), $Trans, durationKey()) $dCdV10prc = $min + 0.1*($max - $min) //Deviation search for when dCdV is above the 10%ile $deviation1 = $dCdV - $dCdV10prc $Above10 = valueSearch($deviation1, 1h, isGreaterThan(0), 0min, true, isLessThan(0), 0min, true) //Calculate filtered derivative of dCdV $dCdVderiv = $dCdV.derivative() //Value Search for Increasing dCdV (positive filtered derivative of dCdV) $dCdVup = $dCdVderiv.validValues().valueSearch(40h, isGreaterThan(0), 30s, isLessThanOrEqualTo(0), 0min) //Composite Conditions to find increasing left side above 10% and right side $LeftSide = $Above10.intersect($dCdVup) $RightSide = $Above10.minus($dCdVup) //Find change in volume over left side and right sides, then divide b/a $VolLeftSide = $Vol.aggregate(delta(), $LeftSide, middleKey()).aggregate(maxValue(), $Trans, middleKey()) $VolRightSide = $Vol.aggregate(delta(), $RightSide, middleKey()).aggregate(maxValue(), $Trans, middleKey()) $VolRightSide/$VolLeftSide Optional Alteration: Multiple Columns It should be noted that oftentimes the conductivity signals are associated to multiple columns in a chromatography system. The chromatography system may switch between two or three columns all reading on the same signal. In order to track changes in column integrity for each column individually, one must assign the transitions to each column prior to performing the Transition Analysis calculations. Multiple methods exist for assigning transitions to each column. Most customers generally have another signal(s) that identify which column is used. This may be valve positions or differential pressure across each column. These signals enable a Value Search (e.g. “Open” or “High Differential Pressure”) to then perform a Composite Condition to automatically assign the columns in use with their transitions. Alternatively, if no signals are present to identify the columns, the columns can be assigned manually through the Custom Condition tool or assigned via a counting system if the order of the columns is constant. An example of Asymmetry calculated for multiple columns is shown below: Content Verified DEC2023
  17. Hi Ivan, There was a post similar to this referring to averaging signals with gaps in the data, which requires both summation and counting of the number of signals (prior to the new .average() operator in R21.0.43). The link to the post is below: Therefore, an equation that could give you the sum of the samples would be the following: $Zero = 0.tosignal(1min) // creates a scalar value of 0 to splice in when the values are invalid (($a.splice($Zero,$a.tocapsules().inverse()))+($b.splice($Zero,$b.tocapsules().inverse()))+($c.splice($Zero,$c.tocapsules().inverse()))+($d.splice($Zero,$d.tocapsules().inverse())))
  18. As a user, I would like to be able to calculate the average multiple signals, particularly when not all signals are always valid across the desired time range. Can someone help me with this?
×
×
  • Create New...