Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revision Previous revision
Next revision
Previous revision
calculate_python_expression [2026/07/18 02:03]
hermann
calculate_python_expression [2026/08/18 20:55] (current)
hermann
Line 3: Line 3:
 ===== Description ===== ===== Description =====
  
-This is a **[[ego_script#​container_functors|container functor]]** that runs a Python instance with the user-defined expression. Like the other calculator functors, data is connected through hook functors placed inside its ''<​nowiki>​{{ … }}</​nowiki>''​ block. ​For an overview of how Dinamica EGO and Python can be linked together, see [[python_coupling|Dinamica EGO and Python Coupling]].+This is a **[[ego_script#​container_functors|container functor]]** that runs a Python instance with the user-defined expression. Like the other calculator functors, data is connected through hook functors placed inside its ''<​nowiki>​{{ … }}</​nowiki>''​ block. ​See also [[calculate_r_expression|Calculate R Expression]] for the equivalent functor using R.
  
 ===== Inputs ===== ===== Inputs =====
  
 ^ Name ^ Type ^ Description ^ ^ Name ^ Type ^ Description ^
-| expression | [[ego_script#​constants|Code]] | The expression that will run on Python. ​''​Code''​ values cannot be written ​as plain text constants in EGO Script ​— see [[#​writing_the_expression_in_ego_script|Writing the expression in EGO Script]] below. |+| expression | [[code_type|Code]] | The expression that will run on Python. ​Written directly ​as a Code constant using its own raw string syntax ​— see [[#​writing_the_expression_in_ego_script|Writing the expression in EGO Script]] below. |
  
 ===== Optional Inputs ===== ===== Optional Inputs =====
Line 24: Line 24:
 ==== Expression inputs ==== ==== Expression inputs ====
  
-Data is passed into the expression through **hook** functors placed inside the container'​s ''<​nowiki>​{{ … }}</​nowiki>''​ block:+Data is passed into the expression through **hook** functors placed inside the container'​s ''<​nowiki>​{{ … }}</​nowiki>''​ block — the same [[ego_script#​verbose_form|verbose form]] hook mechanism used by the calculator functors:
  
   * Tables and lookup tables → [[Number Table]] → available in the expression as ''​dinamica.inputs["​t1"​]'',​ ''​dinamica.inputs["​t2"​]'',​ …, ''​dinamica.inputs["​t100"​]''​   * Tables and lookup tables → [[Number Table]] → available in the expression as ''​dinamica.inputs["​t1"​]'',​ ''​dinamica.inputs["​t2"​]'',​ …, ''​dinamica.inputs["​t100"​]''​
   * Scalar values → [[Number Value]] → available as ''​dinamica.inputs["​v1"​]'',​ ''​dinamica.inputs["​v2"​]'',​ …, ''​dinamica.inputs["​v100"​]''​   * Scalar values → [[Number Value]] → available as ''​dinamica.inputs["​v1"​]'',​ ''​dinamica.inputs["​v2"​]'',​ …, ''​dinamica.inputs["​v100"​]''​
 +  * Strings → [[Number String]] → available as ''​dinamica.inputs["​s1"​]'',​ ''​dinamica.inputs["​s2"​]'',​ …, ''​dinamica.inputs["​s100"​]''​
  
 Maps cannot be connected — this functor has no cell context. There is no shorthand notation. See [[calculate_functors|Calculate Functors — Complete Operator Documentation]] for the general hook mechanism and syntax. Maps cannot be connected — this functor has no cell context. There is no shorthand notation. See [[calculate_functors|Calculate Functors — Complete Operator Documentation]] for the general hook mechanism and syntax.
  
-Every table or lookup table arriving through ''​dinamica.inputs''​ is represented in Python as a **list of lists**: the first inner list contains the column names (the header row) and every subsequent inner list is a row of data. The header ​name of each key column ​has an asterisk (''​*''​) appended ​to it.+Every table or lookup table arriving through ''​dinamica.inputs''​ is represented in Python as a **list of lists**: the first inner list contains the column names (the header row) and every subsequent inner list is a row of data. The header ​carries plain column ​names only — no ''​*'' ​marking key columns, no ''#​type''​ annotation — so which columns were keys in the original table is information Python doesn'​t receive and can't recover from the input alone. Code that needs that distinction has to be told it separately, for instance by also connecting the key count as its own [[Number Value]] hook.
  
 ==== Expression outputs ==== ==== Expression outputs ====
Line 37: Line 38:
 Values are returned to Dinamica by assigning them into ''​dinamica.outputs'',​ keyed by the desired output name. Every assigned value becomes an entry in the output ''​result''​ struct: Values are returned to Dinamica by assigning them into ''​dinamica.outputs'',​ keyed by the desired output name. Every assigned value becomes an entry in the output ''​result''​ struct:
  
-<​code>​+<​code ​python>
 // Scalar values are assigned directly // Scalar values are assigned directly
 dinamica.outputs["​patchCount"​] = 42 dinamica.outputs["​patchCount"​] = 42
Line 56: Line 57:
 | [[Extract Struct Tuple]] | A tuple value | | [[Extract Struct Tuple]] | A tuple value |
  
-Each functor takes two inputs: the ''​Struct''​ returned by ''​CalculatePythonExpression'',​ and the name of the entry to extract as a string constant. For example, to retrieve a numeric output named ''​patchCount''​ and a table output named ''​filteredPatches'':​+Each functor takes two inputs: the ''​Struct''​ returned by ''​CalculatePythonExpression'',​ and the name of the entry to extract as a string constant. For example, to retrieve a numeric output named ''​patchCount''​ and a table output named ''​filteredPatches'' ​— the ''​$"​(...)"''​ raw string syntax wrapping the Python code is explained in [[#​writing_the_expression_in_ego_script|Writing the expression in EGO Script]] below:
  
 <​code>​ <​code>​
-result := CalculatePythonExpression ​(String ​$"(+result := CalculatePythonExpression $"(
 myTable = [["​PatchId*",​ "​Area"​],​ [1, 12.4], [2, 8.7]] myTable = [["​PatchId*",​ "​Area"​],​ [1, 12.4], [2, 8.7]]
 dinamica.outputs['​patchCount'​] = 42 dinamica.outputs['​patchCount'​] = 42
 dinamica.outputs['​filteredPatches'​] = dinamica.prepareTable(myTable,​ 1) dinamica.outputs['​filteredPatches'​] = dinamica.prepareTable(myTable,​ 1)
-)"{{ }};+)" {{ }};
 patchCount ​     := ExtractStructNumber result "​patchCount";​ patchCount ​     := ExtractStructNumber result "​patchCount";​
 filteredPatches := ExtractStructTable ​ result "​filteredPatches";​ filteredPatches := ExtractStructTable ​ result "​filteredPatches";​
Line 70: Line 71:
 ==== Utilities ==== ==== Utilities ====
  
-=== dinamica.package() ​===+=== Installing packages ​===
  
-Installs ​(if needed via pip) and imports the requested module.+Packages can be installed in two ways: calling ''​dinamica.package(...)''​ from within the expression, or listing package names on the //​packages//​ input port. The two differ in timing: ''​dinamica.package(...)''​ runs **during** the expression, so it can be called conditionally;​ the //​packages//​ port always runs **before** the expression executes. 
 + 
 +**dinamica.package()** 
 + 
 +''​dinamica.package(packageName,​ installPath=None,​ loadPath=None)''​ installs ​(if needed via pip) and imports the requested module. Because it runs together with the script, it may fail if a package is already loaded in an incompatible version.
  
 ^ Parameter ^ Type ^ Default ^ Description ^ ^ Parameter ^ Type ^ Default ^ Description ^
Line 78: Line 83:
 | ''​installPath''​ | str | ''​packageName''​ | What is passed to ''​pip install''​. Can be a plain name, a version-pinned requirement,​ a wheel filename or URL, a ''​git+''​ URL, or a name followed by extra pip flags. | | ''​installPath''​ | str | ''​packageName''​ | What is passed to ''​pip install''​. Can be a plain name, a version-pinned requirement,​ a wheel filename or URL, a ''​git+''​ URL, or a name followed by extra pip flags. |
 | ''​loadPath''​ | str | ''​packageName''​ | The name used to ''​import''​ the module in Python. | | ''​loadPath''​ | str | ''​packageName''​ | The name used to ''​import''​ the module in Python. |
- 
-=== dinamica.prepareTable() === 
- 
-Converts a list of lists into a table ready to be assigned to an output. The first inner list must be the header row. 
- 
-^ Parameter ^ Type ^ Default ^ Description ^ 
-| ''​inputTable''​ | list of lists | — | The table data. First inner list is the header row; subsequent inner lists are data rows. | 
-| ''​numKeys''​ | int | — | Number of key columns. Key column names will have ''​*''​ appended in the output. | 
- 
-=== dinamica.prepareLookupTable() === 
- 
-Converts a list of lists into a lookup table ready to be assigned to an output. The first inner list must be the header row. 
- 
-^ Parameter ^ Type ^ Default ^ Description ^ 
-| ''​lut''​ | list of lists | — | The lookup table data. First inner list is the header row; subsequent inner lists are data rows. | 
- 
-=== dinamica.toTable() === 
- 
-Converts several Python data shapes into a valid Dinamica table for output. 
- 
-^ Parameter ^ Type ^ Default ^ Description ^ 
-| ''​inputTable''​ | list of lists; dict of lists; list of tuples; flat list; ''​pandas.DataFrame'';​ ''​numpy.array''​ | — | The data to convert. A flat list produces a lookup table with sequential keys. A ''​numpy.array''​ must have its header as the first row. | 
- 
-=== Installing packages === 
- 
-Packages can be installed in two ways: 
- 
-  * Calling ''​dinamica.package(...)''​ from within the expression. 
-  * Listing package names on the //​packages//​ input port, one per line. 
- 
-These two approaches differ in timing: ''​dinamica.package(...)''​ runs **during** the expression, so it can be called conditionally;​ the //​packages//​ port always runs **before** the expression executes. Because ''​dinamica.package(...)''​ runs together with the script, it may fail if a package is already loaded in an incompatible version. 
- 
-''​dinamica.package(packageName,​ installPath=None,​ loadPath=None)'':​ 
- 
-  * ''​packageName''​ identifies the package and is the default for both following parameters when omitted. 
-  * ''​installPath''​ is passed to ''​pip install''​. It can be a version-pinned requirement,​ a wheel filename or URL, a ''​git+''​ URL, or a name plus extra pip flags. 
-  * ''​loadPath''​ is the name used to ''​import''​ the module. It defaults to ''​packageName''​. 
  
 Simply install and load ''​numpy'':​ Simply install and load ''​numpy'':​
-<​code>​+<​code ​python>
 dinamica.package("​numpy"​) dinamica.package("​numpy"​)
 </​code>​ </​code>​
  
 Specify a version: Specify a version:
-<​code>​+<​code ​python>
 dinamica.package("​numpy",​ "​numpy==1.19.5"​) dinamica.package("​numpy",​ "​numpy==1.19.5"​)
 </​code>​ </​code>​
  
 Install a package whose importable name differs from its pip name: Install a package whose importable name differs from its pip name:
-<​code>​+<​code ​python>
 dinamica.package("​segment_anything_py",​ "​segment_anything_py",​ "​segment_anything"​) dinamica.package("​segment_anything_py",​ "​segment_anything_py",​ "​segment_anything"​)
 </​code>​ </​code>​
  
 Use arbitrary pip parameters, such as installing from a remote wheel with a custom index: Use arbitrary pip parameters, such as installing from a remote wheel with a custom index:
-<​code>​+<​code ​python>
 dinamica.package("​segment_anything_py",​ "​https://​files.pythonhosted.org/​packages/​43/​2f/​dabe75d90a7eb54a0a609a0fc5c36d1933256319beaea5d6b2f176e213a2/​segment_anything_py-1.0-py3-none-any.whl --index-url https://​download.pytorch.org/​whl/​cu118",​ "​segment_anything"​) dinamica.package("​segment_anything_py",​ "​https://​files.pythonhosted.org/​packages/​43/​2f/​dabe75d90a7eb54a0a609a0fc5c36d1933256319beaea5d6b2f176e213a2/​segment_anything_py-1.0-py3-none-any.whl --index-url https://​download.pytorch.org/​whl/​cu118",​ "​segment_anything"​)
 </​code>​ </​code>​
  
 Chain several installs: Chain several installs:
-<​code>​+<​code ​python>
 dinamica.package("​cython"​) dinamica.package("​cython"​)
 dinamica.package("​numpy"​) dinamica.package("​numpy"​)
Line 143: Line 111:
 </​code>​ </​code>​
  
-The //​packages//​ port takes one package identifier per line, in any form accepted by ''​pip''​. Unlike ''​dinamica.package()'',​ it does not support specifying separate install and import ​names, and every listed ​package ​is installed before ​the expression runs:+**The packages port** 
 + 
 +The //​packages// ​input port takes one package identifier per line, in any form accepted by ''​pip''​. Every listed package is installed before the expression runs. Unlike ''​dinamica.package()'', ​the port only installs — it does not also import the module, and it does not support specifying ​separate install ​name and import ​name. The expression must still import each package ​itself, using its actual importable name (which may differ from the name given to ''​pip''​). 
 + 
 +The //​packages//​ port:
 <​code>​ <​code>​
 numpy numpy
Line 150: Line 122:
 </​code>​ </​code>​
  
-==== Writing the expression ​in EGO Script ====+The expression
 +<code python>​ 
 +import numpy 
 +import requests 
 +import torchvision 
 +</​code>​
  
-When writing a script by hand, the ''​expression''​ input cannot be filled in directly as a text constant: the [[ego_script#​constants|Code]] type is represented in the underlying script using base64 encoding, which is impractical to write or edit directly. Instead, connect a ''​String''​ carrier functor containing the expression text to the ''​expression''​ port — its output is accepted wherever a ''​Code''​ value is expected. Since only this one output is needed, the carrier can be [[ego_script#​inline_syntax|inlined]] directly into the call.+=== dinamica.prepareTable() ===
  
-The following counts ​the patches in a land cover areas table whose area meets a minimum threshold:+Converts a list of lists into a table ready to be assigned to an output. ​The first inner list must be the header row. As with any table (see [[ego_script#​constants|Constants]]),​ each column'​s type is inferred from its own valuesa column of ''​int''/''​float''​ values produces a Real column, a column of ''​str''​ values produces a String column.
  
-<​code>​ +^ Parameter ^ Type ^ Default ^ Description ^ 
-result := CalculatePythonExpression (String $"( +| ''inputTable'' | list of lists | — | The table data. First inner list is the header ​row; subsequent inner lists are data rows
-total = 0 +''​numKeys''​ | int | — | Number of key columnsKey column names will have ''​*''​ appended in the output. |
-for row in dinamica.inputs['t1'][1:]: +
-    if row[1] >= dinamica.inputs['v1']: +
-        total += 1 +
-dinamica.outputs['patchCount'] = total +
-)") {{ +
-    NumberTable landCoverAreas 1; +
-    NumberValue minimumArea ​   1; +
-}}; +
-patchCount := ExtractStructNumber result "​patchCount";​ +
-</​code>​+
  
-''​CalculatePythonExpression'' ​returns a ''​Struct'' ​containing all values assigned to ''​dinamica.outputs''​. The ''​ExtractStructNumber'' ​functor then pulls the ''​patchCount'' ​entry out of that struct by nameSee [[#​retrieving_outputs|Retrieving outputs]] for the full list of extraction functors.+=== dinamica.prepareLookupTable() === 
 + 
 +Converts a list of lists into a lookup table ready to be assigned to an output. The first inner list must be the header row. Lookup tables are always Real-typed on both key and value sides — there is no String option — so every value in ''​lut'' ​must be numeric. 
 + 
 +^ Parameter ^ Type ^ Default ^ Description ^ 
 +''​lut'' ​| list of lists | — | The lookup table data. First inner list is the header row; subsequent inner lists are data rows. | 
 + 
 +=== dinamica.toTable() === 
 + 
 +Converts several Python data shapes into a valid Dinamica table for output. Column types are inferred the same way as ''​dinamica.prepareTable()'' ​— except for a flat list, which always produces a Real-typed lookup table with sequential keys, matching the Real-only rule for lookup tables. 
 + 
 +^ Parameter ^ Type ^ Default ^ Description ^ 
 +| ''​inputTable''​ | list of lists; dict of lists; list of tuples; flat list; ''​pandas.DataFrame'';​ ''​numpy.array''​ | — | The data to convert. A flat list produces a lookup table with sequential keys. A ''​numpy.array'' ​must have its header as the first row. | 
 +''​numKeys'' ​| int | — | Number ​of key columnsKey column names will have ''​*''​ appended in the output. Ignored for a flat list, which always produces a lookup table with sequential keys|
  
 ==== Examples ==== ==== Examples ====
Line 182: Line 162:
 Install and import ''​numpy'',​ then inspect all inputs passed in by Dinamica: Install and import ''​numpy'',​ then inspect all inputs passed in by Dinamica:
  
-<​code>​+<​code ​python>
 dinamica.package("​numpy"​) dinamica.package("​numpy"​)
 print(dinamica.inputs) print(dinamica.inputs)
 </​code>​ </​code>​
 +
 +> **Note:** The ''​print()''​ call is visible in Dinamica EGO's Message Log, shown as a Result-level message. Log levels are ordered Unconditional,​ Error, Warning, Result, Info, Info2, Debug, Debug2 — messages printed from Python are only shown when the Message Log level is set to Result or a more verbose level; at Unconditional,​ Error, or Warning they are suppressed.
  
 Print the rows of both connected tables to verify their contents: Print the rows of both connected tables to verify their contents:
  
-<​code>​+<​code ​python>
 for row in dinamica.inputs["​t1"​]:​ for row in dinamica.inputs["​t1"​]:​
     print(row)     print(row)
Line 199: Line 181:
 Count the patches whose area meets the minimum threshold and sum their total area: Count the patches whose area meets the minimum threshold and sum their total area:
  
-<​code>​+<​code ​python>
 patchCount = 0 patchCount = 0
 totalArea = 0.0 totalArea = 0.0
Line 213: Line 195:
 Return a filtered table containing only the patches above the threshold: Return a filtered table containing only the patches above the threshold:
  
-<​code>​+<​code ​python>
 filtered = [dinamica.inputs["​t1"​][0]] filtered = [dinamica.inputs["​t1"​][0]]
 for row in dinamica.inputs["​t1"​][1:​]:​ for row in dinamica.inputs["​t1"​][1:​]:​
Line 224: Line 206:
 Compute the total area per category and return it as a lookup table: Compute the total area per category and return it as a lookup table:
  
-<​code>​+<​code ​python>
 areaSums = {} areaSums = {}
 for row in dinamica.inputs["​t1"​][1:​]:​ for row in dinamica.inputs["​t1"​][1:​]:​
Line 237: Line 219:
 </​code>​ </​code>​
  
-===== Group =====+==== Writing the expression in EGO Script ​==== 
 + 
 +The ''​expression''​ input can be filled in directly as a text constant, using [[code_type|Code Type]]'​s own raw string syntax — the same ''​$"<​delimiter>​( raw_characters )<​delimiter>"''​ form used by String constants. This is also the form the Dinamica EGO GUI's dedicated code editor generates when it writes the ''​expression''​ port's value, so a hand-written script and one produced by the GUI take the same shape. See [[code_type|Code Type]] for the full grammar, including the base64 alternative form. 
 + 
 +The following counts the patches in a land cover areas table whose area meets a minimum threshold:​ 
 + 
 +<​code>​ 
 +result :CalculatePythonExpression $"( 
 +total 
 +for row in dinamica.inputs['​t1'​][1:​]:​ 
 +    if row[1] >= dinamica.inputs['​v1'​]:​ 
 +        total += 1 
 +dinamica.outputs['​patchCount'​] = total 
 +)" {{ 
 +    NumberTable landCoverAreas 1; 
 +    NumberValue minimumArea ​   1; 
 +}}; 
 +patchCount := ExtractStructNumber result "​patchCount";​ 
 +</​code>​
  
-[[Functor List#Integration|Integration]]+''​CalculatePythonExpression''​ returns a ''​Struct''​ containing all values assigned to ''​dinamica.outputs''​. The ''​ExtractStructNumber''​ functor then pulls the ''​patchCount''​ entry out of that struct by name. See [[#retrieving_outputs|Retrieving outputs]] for the full list of extraction functors.
  
-===== Internal Name =====+A more involved example: installing ''​numpy''​ to compute area statistics for a land cover patches table and flag outlier patches, then retrieving both the scalar statistics and the resulting table:
  
-CalculatePythonExpression+<​code>​ 
 +result := CalculatePythonExpression ​$"( 
 +dinamica.package('​numpy'​)
  
-===== See Also =====+areas numpy.array([row[1] for row in dinamica.inputs['​t1'​][1:​]])
  
-  * [[python_coupling|Dinamica EGO and Python Coupling]] +meanArea = float(numpy.mean(areas)
-  * [[ego_script#​container_functors|EGO Script — Container Functors]] +stdArea = float(numpy.std(areas))
-  * [[ego_script#​verbose_form|EGO Script — Verbose Form (Hooks)]] +
-  * [[calculate_functors|Calculate Functors — Complete Operator Documentation]]+
  
 +isOutlier = numpy.abs(areas - meanArea) > 2 * stdArea
  
 +dinamica.outputs['​meanArea'​] = meanArea
 +dinamica.outputs['​stdArea'​] = stdArea
 +
 +header = dinamica.inputs['​t1'​][0]
 +outlierRows = [row for row, flagged in zip(dinamica.inputs['​t1'​][1:​],​ isOutlier) if flagged]
 +outlierTable = [header] + outlierRows
 +
 +dinamica.outputs['​outlierPatches'​] = dinamica.prepareTable(outlierTable,​ 1)
 +)" {{
 +    NumberTable landCoverPatches 1;
 +}};
 +meanArea ​      := ExtractStructNumber result "​meanArea";​
 +stdArea ​       := ExtractStructNumber result "​stdArea";​
 +outlierPatches := ExtractStructTable ​ result "​outlierPatches";​
 +</​code>​
 +
 +Here ''​landCoverPatches''​ is bound to ''​t1''​ through a single [[Number Table]] hook.
 +
 +===== Group =====
 +
 +[[Functor List#​Integration|Integration]]
 +
 +===== Internal Name =====
 +
 +CalculatePythonExpression