Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
|
manipulating_tables_and_lookup_tables [2026/08/03 01:57] hermann |
manipulating_tables_and_lookup_tables [2026/08/18 03:09] (current) hermann [Printing a table with any number of key columns] |
||
|---|---|---|---|
| Line 500: | Line 500: | ||
| ===== Table Manager ===== | ===== Table Manager ===== | ||
| - | The accumulator pattern in [[#storing_results_across_a_loop|Storing results across a loop]] builds a table one row at a time with a Mux, which forces the loop to run sequentially. **Table Manager** solves a related but different problem: building a table with an extra key column from sub-tables produced inside a loop, without threading every sub-table through a Mux. | + | The accumulator pattern in [[#storing_results_across_a_loop|Storing results across a loop]] uses a Mux to carry a growing table from one iteration to the next -- which, as that section's closing note explains, forces the loop to run sequentially, one iteration at a time. **Table Manager** solves the same class of problem -- building a table with an extra key column from results computed across a loop -- without that cost: each iteration registers its own sub-table independently, with no Mux and nothing threaded between iterations, so the loop producing those partial results stays eligible to run in parallel, per [[basic_data_flow|Basic Data Flow]]. A single [[#mergesubtables|MergeSubTables]] call combines everything afterward. |
| - | Three functors work together: | + | A container and two functors do the core work; a fourth covers a narrower case: |
| ==== TableManager ==== | ==== TableManager ==== | ||
| Line 540: | Line 540: | ||
| Each sub-table's ''keys'' tuple can hold more than one element; every element becomes its own leading key column on ''baseTable''. A ''tableName'' can only be merged once -- the merge removes its sub-tables from the manager as it runs. | Each sub-table's ''keys'' tuple can hold more than one element; every element becomes its own leading key column on ''baseTable''. A ''tableName'' can only be merged once -- the merge removes its sub-tables from the manager as it runs. | ||
| - | > **Warning:** ''MergeSubTables'' has no data dependency on the ''SubTable'' calls that feed it -- it reads ''emptyPriceTable'', the literal ''tableName'', and the auto-bound manager, none of which depend on the loop that registered the sub-tables. Placing it after a loop in the script, even inside the same ''TableManager'' container, does **not** guarantee it runs after that loop finishes; the two have no connection, so the engine is free to run them in either order or at the same time. Force the ordering explicitly with a [[Group]] gated by the loop's ''sequenceOutput'', as in the example below -- see [[basic_data_flow#ordering_without_data_sequence_connections|Ordering without data: sequence connections]] and [[ego_script#sequence_ports|Sequence ports]]. | + | > **Warning:** ''MergeSubTables'' has no data dependency on the ''SubTable'' calls that feed it -- its ''baseTable'' input, its literal ''tableName'', and its auto-bound manager are all available from the moment the container starts, and none of them depend on the loop that registered the sub-tables. Placing it after a loop in the script, even inside the same ''TableManager'' container, does **not** guarantee it runs after that loop finishes; the two have no connection, so the engine is free to run them in either order or at the same time. Force the ordering explicitly with a [[Group]] gated by the loop's ''sequenceOutput'', as in the example below -- see [[basic_data_flow#ordering_without_data_sequence_connections|Ordering without data: sequence connections]] and [[ego_script#sequence_ports|Sequence ports]]. |
| ==== TableManagerValue ==== | ==== TableManagerValue ==== | ||
| Line 565: | Line 565: | ||
| // Stand-in for a per-year computation; in practice this might come | // Stand-in for a per-year computation; in practice this might come | ||
| // from a database query, a Calculate Table Values call, or another | // from a database query, a Calculate Table Values call, or another | ||
| - | // model entirely. | + | // model entirely. A Table literal only accepts literal cell values, |
| + | // so a real per-year computation would build this row by row with | ||
| + | // AddTupleValue and AddTableRow instead, the same as any table | ||
| + | // whose contents aren't known until the model runs. | ||
| yearPrices := Table [ | yearPrices := Table [ | ||
| "City*", "Price", | "City*", "Price", | ||
| - | "Boston", $ [ 4000 + $year ], | + | "Boston", 4000, |
| - | "Chelsea", $ [ 200 + $year ] | + | "Chelsea", 200 |
| ]; | ]; | ||
| Line 582: | Line 585: | ||
| }}; | }}; | ||
| }}; | }}; | ||
| - | |||
| - | // priceTable is usable here, once the Group it was produced inside -- and | ||
| - | // through its sequenceInput, the loop that Group was forced to wait for -- | ||
| - | // has finished. | ||
| </code> | </code> | ||
| + | |||
| + | ''priceTable'' is usable here, once the Group it was produced inside — and through its ''sequenceInput'', the loop that Group was forced to wait for — has finished. | ||
| The loop's own iterations are still eligible to run in parallel with each other: nothing inside it is a Mux, nothing inside it is consumed from outside it, and it feeds no submodel output port -- see [[basic_data_flow|Basic Data Flow]]. Only the loop as a whole is sequenced against the merge, which is why this avoids the cost the Mux-accumulator pattern above pays on every single iteration. | The loop's own iterations are still eligible to run in parallel with each other: nothing inside it is a Mux, nothing inside it is consumed from outside it, and it feeds no submodel output port -- see [[basic_data_flow|Basic Data Flow]]. Only the loop as a whole is sequenced against the merge, which is why this avoids the cost the Mux-accumulator pattern above pays on every single iteration. | ||
| Line 592: | Line 593: | ||
| > **Note:** ''year'' is passed to ''SubTable'' directly, not wrapped in brackets as ''[year]''. Bracket syntax is for Tuple **literals**; a connected Real value converts to a one-element Tuple automatically on connection, the same pattern used by ''GetTableFromKey'' in [[#sub-tables|Sub-Tables]] above. | > **Note:** ''year'' is passed to ''SubTable'' directly, not wrapped in brackets as ''[year]''. Bracket syntax is for Tuple **literals**; a connected Real value converts to a one-element Tuple automatically on connection, the same pattern used by ''GetTableFromKey'' in [[#sub-tables|Sub-Tables]] above. | ||
| - | There is no functor exposing ''TableManager'''s ''eraseSubTables'' or ''getTableCount'' methods -- those are internal bookkeeping, not part of the functor catalog. | ||
| ===== Printing a table with a generic format ===== | ===== Printing a table with a generic format ===== | ||
| Line 600: | Line 600: | ||
| [[Get Table Row]], [[Get Tuple Size]], and [[Get Tuple Value]] make the column-count side of this possible: a row read as a Tuple can be measured with ''GetTupleSize'', and each of its elements retrieved by position, rather than by column name, with ''GetTupleValue''. Each retrieved element has type ''TableValue'' — a generic type able to hold either a Real or a String. | [[Get Table Row]], [[Get Tuple Size]], and [[Get Tuple Value]] make the column-count side of this possible: a row read as a Tuple can be measured with ''GetTupleSize'', and each of its elements retrieved by position, rather than by column name, with ''GetTupleValue''. Each retrieved element has type ''TableValue'' — a generic type able to hold either a Real or a String. | ||
| - | [[String]] and [[Real Value]] each convert a ''TableValue'' the same conditional way: the conversion only succeeds if the ''TableValue'' actually holds that type, and raises an error otherwise — converting an already-typed, known ''RealValue'' or String, by contrast, never fails. Attempting the String conversion wrapped in [[Skip On Error]] turns that failure into a type test: ''SkipOnError'' catches the error rather than aborting the model, and its ''executionCompletedSucessfully'' output reports whether the ''TableValue'' really was a String. [[If Then]] and [[If Not Then]] branch on that result and take the matching path. The first uses the converted String directly. The second has already ruled String out, so it extracts a ''RealValue'' instead — safe now, since only Real remains possible — and converts //that// known value to a string. A [[String Junction]] then recombines the two branches into one result, since only one of them ever actually runs. | + | [[String]] and [[Real Value]] each convert a ''TableValue'' the same conditional way: the conversion only succeeds if the ''TableValue'' actually holds that type, and raises an error otherwise — converting an already-typed, known ''RealValue'' or String, by contrast, never fails. Wrapping each attempt in its own [[Skip On Error]] turns that failure into "produced nothing" instead of aborting the model, so only the attempt that actually matches the ''TableValue'''s real type ever produces a display string; the other attempt simply fails silently and produces nothing. A [[String Junction]] then picks whichever one exists. |
| + | |||
| + | > **Note:** A junction always tries its first port before its second — see [[ego_script#carrying_and_selecting_values_across_iterations|Carrying and selecting values across iterations]] for this rule stated generally, since it applies identically to every Junction functor (''String'', ''Table'', ''Value'', ''Lookup Table'', ''Map'', ''Categorical Map'', ''Folder''). It makes no practical difference here, though: the two attempts below are mutually exclusive by construction, so only one of ''stringDisplay''/''realDisplay'' is ever available for the junction to pick regardless of port order. | ||
| <code> | <code> | ||
| Line 626: | Line 628: | ||
| displayColumn := $ [ $columnIndex - 1 ]; | displayColumn := $ [ $columnIndex - 1 ]; | ||
| - | // Attempt to read the cell as a String; SkipOnError catches the | + | // Try the cell as a String; SkipOnError leaves stringDisplay |
| - | // failure if it's actually a Real, rather than aborting. | + | // unproduced if cellValue is actually a Real, rather than |
| - | attempt := SkipOnError .yes {{ | + | // aborting the model. |
| - | asString := String cellValue; | + | _ := SkipOnError .yes {{ |
| - | }}; | + | stringDisplay := String cellValue; |
| - | + | ||
| - | _ := IfThen attempt {{ | + | |
| - | stringDisplay := String asString; | + | |
| }}; | }}; | ||
| - | _ := IfNotThen attempt {{ | + | // Try the cell as a Real instead; SkipOnError leaves |
| - | // cellValue was a Real instead; RealValue extracts it, and | + | // realDisplay unproduced if cellValue is actually a String. |
| - | // String — safe here, since converting a known Real never | + | _ := SkipOnError .yes {{ |
| - | // fails — turns it into a display string. | + | |
| asReal := RealValue cellValue; | asReal := RealValue cellValue; | ||
| realDisplay := String asReal; | realDisplay := String asReal; | ||
| }}; | }}; | ||
| - | // Only one of stringDisplay/realDisplay was actually produced; | + | // Exactly one of the two attempts above ever succeeds; |
| - | // StringJunction picks whichever one has a value. | + | // StringJunction picks whichever one was produced. |
| cellDisplay := StringJunction stringDisplay realDisplay; | cellDisplay := StringJunction stringDisplay realDisplay; | ||
| Line 1047: | Line 1045: | ||
| ]; | ]; | ||
| - | result := CalculatePythonExpression (String $"( | + | result := CalculatePythonExpression $"( |
| inputTable = dinamica.inputs['t1'] | inputTable = dinamica.inputs['t1'] | ||
| header = inputTable[0] | header = inputTable[0] | ||
| Line 1060: | Line 1058: | ||
| dinamica.outputs['reshapedTable'] = dinamica.prepareTable(newTable, 1) | dinamica.outputs['reshapedTable'] = dinamica.prepareTable(newTable, 1) | ||
| - | )") {{ | + | )" {{ |
| NumberTable myTable 1; | NumberTable myTable 1; | ||
| }}; | }}; | ||
| Line 1082: | Line 1080: | ||
| // Same generic Real/String printing technique as the earlier | // Same generic Real/String printing technique as the earlier | ||
| - | // example: attempt String first, SkipOnError catches the | + | // example: try both conversions independently, each guarded by |
| - | // failure if it's actually a Real, RealValue then converts it. | + | // its own SkipOnError, and let StringJunction pick whichever |
| - | attempt := SkipOnError .yes {{ | + | // one was actually produced. |
| - | asString := String cellValue; | + | _ := SkipOnError .yes {{ |
| + | stringDisplay := String cellValue; | ||
| }}; | }}; | ||
| - | _ := IfThen attempt {{ | + | _ := SkipOnError .yes {{ |
| - | stringDisplay := String asString; | + | |
| - | }}; | + | |
| - | + | ||
| - | _ := IfNotThen attempt {{ | + | |
| asReal := RealValue cellValue; | asReal := RealValue cellValue; | ||
| realDisplay := String asReal; | realDisplay := String asReal; | ||
| Line 1118: | Line 1113: | ||
| Reach for a general Table instead when a single Real value per Real key isn't enough — when a row needs more than one associated value, or a String value, or a String key, or when rows need to be indexed by more than one key at once. A String key column costs more than the type change alone: iterating one means mapping each placeholder index back to its actual key inside the loop, so prefer a Real key column wherever the data allows. | Reach for a general Table instead when a single Real value per Real key isn't enough — when a row needs more than one associated value, or a String value, or a String key, or when rows need to be indexed by more than one key at once. A String key column costs more than the type change alone: iterating one means mapping each placeholder index back to its actual key inside the loop, so prefer a Real key column wherever the data allows. | ||
| - | |||