Differences
This shows you the differences between two versions of the page.
| Both sides previous revision Previous revision Next revision | Previous revision | ||
|
calculate_functors [2026/07/15 01:13] hermann |
calculate_functors [2026/07/29 13:42] (current) hermann |
||
|---|---|---|---|
| Line 172: | Line 172: | ||
| Unless a restriction is noted, every operator is available in all five calculator functors. | Unless a restriction is noted, every operator is available in all five calculator functors. | ||
| + | |||
| + | Every construct in the language — including a conditional — is itself just an expression that resolves to one value, and any expression can appear anywhere another one is expected. This makes conditionals freely composable: two ''if...then...else'' results can be summed directly, a conditional's own test can be replaced with another conditional, and so on, without any special syntax for combining them. For example, ''if (if i1 > 0 then 1 else i2) then i2 else 1 + i3'' nests one conditional inside another's test. | ||
| ===== Identifier Syntax ===== | ===== Identifier Syntax ===== | ||
| Line 375: | Line 377: | ||
| > **Availability:** [[Calculate Map]] and [[Calculate Categorical Map]] only. | > **Availability:** [[Calculate Map]] and [[Calculate Categorical Map]] only. | ||
| - | These operators compute a neighbourhood statistic and then use the result as an **index** to retrieve a value from either a lookup table or a second image. | + | These operators compute a neighbourhood statistic and then use the result as an **index** to retrieve a value from either a connected Lookup Table or a second image. ''TABLE'' below always refers to a **Lookup Table** specifically — see section 5 for the operators that query one directly. |
| + | |||
| + | > If ''TABLE'' is bound to a multi-column Table (not a Lookup Table), an error is reported. See section 6 for multi-column tables. | ||
| **Full syntax:** \\ | **Full syntax:** \\ | ||
| Line 385: | Line 389: | ||
| ''nbN(IMAGE, h, w, IMAGE2)'' | ''nbN(IMAGE, h, w, IMAGE2)'' | ||
| - | When a **TABLE** is provided: the statistic is used as a key to query the lookup table. \\ | + | When a **TABLE** (Lookup Table) is provided: the statistic is used as a key to query it, equivalent to ''TABLE[statistic]'' from section 5. \\ |
| When an **IMAGE2** is provided: the statistic is used as a pixel coordinate to sample ''IMAGE2''. | When an **IMAGE2** is provided: the statistic is used as a pixel coordinate to sample ''IMAGE2''. | ||
| ^ Operator ^ Syntax ^ Description ^ Example ^ | ^ Operator ^ Syntax ^ Description ^ Example ^ | ||
| - | | MinRef | ''nbMinRef(...)'' | Finds the minimum in the window, then uses it as an index. | ''nbMinRef(i4, 2, 3, t1, line, column)'' | | + | | MinRef | ''nbMinRef(...)'' | Finds the minimum in the window, then uses it as an index into the Lookup Table (or a coordinate into IMAGE2). | ''nbMinRef(i4, 2, 3, t1, line, column)'' | |
| - | | MaxRef | ''nbMaxRef(...)'' | Finds the maximum in the window, then uses it as an index. | ''nbMaxRef(i4, 2, 3, i1, line, column)'' | | + | | MaxRef | ''nbMaxRef(...)'' | Finds the maximum in the window, then uses it as an index into the Lookup Table (or a coordinate into IMAGE2). | ''nbMaxRef(i4, 2, 3, i1, line, column)'' | |
| ---- | ---- | ||
| Line 401: | Line 405: | ||
| **Syntax:** ''nbConvol(IMAGE, TABLE, h, w)'' / ''nbConvol(IMAGE, TABLE, h, w, y, x)'' | **Syntax:** ''nbConvol(IMAGE, TABLE, h, w)'' / ''nbConvol(IMAGE, TABLE, h, w, y, x)'' | ||
| + | |||
| + | > If ''TABLE'' is bound to a multi-column Table (not a Lookup Table), an error is reported. See section 6 for multi-column tables. | ||
| ^ Parameter ^ Meaning ^ | ^ Parameter ^ Meaning ^ | ||
| | ''IMAGE'' | The image to convolve (''iX'') | | | ''IMAGE'' | The image to convolve (''iX'') | | ||
| - | | ''TABLE'' | A Table whose entries are the kernel coefficients, indexed 1 to h*w, mapped **left-to-right, top-to-bottom** | | + | | ''TABLE'' | A **Lookup Table** whose entries are the kernel coefficients, indexed 1 to h*w, mapped **left-to-right, top-to-bottom**. | |
| | ''h'' | Number of kernel rows | | | ''h'' | Number of kernel rows | | ||
| | ''w'' | Number of kernel columns | | | ''w'' | Number of kernel columns | | ||
| | ''y'', ''x'' | Anchor position (optional; defaults to window centre) | | | ''y'', ''x'' | Anchor position (optional; defaults to window centre) | | ||
| - | Each cell in the window is assigned a unique integer key, starting at 1 and advancing left-to-right, then top-to-bottom. The ''TABLE'' argument must contain one row per window cell, where the key is that cell's index and the value is its convolution coefficient. The result of ''nbConvol'' is the weighted sum of all pixel values in the window, each multiplied by the coefficient stored at the corresponding key in the table. | + | Each cell in the window is assigned a unique integer key, starting at 1 and advancing left-to-right, then top-to-bottom. The ''TABLE'' argument must be a Lookup Table containing one row per window cell, where the key is that cell's index and the value is its convolution coefficient. The result of ''nbConvol'' is the weighted sum of all pixel values in the window, each multiplied by the coefficient stored at the corresponding key in the table. |
| The grids below illustrate the index mapping for some common window sizes: | The grids below illustrate the index mapping for some common window sizes: | ||
| Line 464: | Line 470: | ||
| The poison spreads through the entire chain: if ''i1'' is null, then ''i1 + 5'' is null, ''(i1 + 5) * v1'' is null, and so on. There is no way to "un-null" a result once a null has entered the calculation — except by explicitly testing for it first. | The poison spreads through the entire chain: if ''i1'' is null, then ''i1 + 5'' is null, ''(i1 + 5) * v1'' is null, and so on. There is no way to "un-null" a result once a null has entered the calculation — except by explicitly testing for it first. | ||
| + | |||
| + | A null value inside an ''if...then...else'' test propagates the same way, regardless of which branch would otherwise have been taken. In ''(if (i1 / 3 > 9) then i2 else 1 + i3) + 10'', a null ''i1'' makes ''i1 / 3'' null, which makes the comparison ''i1 / 3 > 9'' null, which makes the whole conditional null — and adding 10 to that null result is still null. Wrapping the expression in ''?'' catches it: ''( (if (i1 / 3 > 9) then i2 else 1 + i3) + 10 ) ? 20'' evaluates to 20 whenever the inner expression comes out null for any reason, including a null test clause. | ||
| In a realistic expression, null can enter from several independent sources simultaneously. This expression computes a weighted transition probability from two input maps and a per-class calibration table: | In a realistic expression, null can enter from several independent sources simultaneously. This expression computes a weighted transition probability from two input maps and a per-class calibration table: | ||
| Line 493: | Line 501: | ||
| > **Note:** ''isNull()'' is immune to the poisoning rule — it always returns 0 or 1, never null, even when its argument is null. It is the only reliable way to branch on null. | > **Note:** ''isNull()'' is immune to the poisoning rule — it always returns 0 or 1, never null, even when its argument is null. It is the only reliable way to branch on null. | ||
| + | |||
| + | This poisoning behaviour is what makes short expressions like ''i1 + 10'' or ''i1 * i2'' work correctly across a whole map without any explicit null-checking: a cell that's null in either input becomes null in the output automatically, so the expression effectively only touches cells where every input is valid. | ||
| + | |||
| + | **Substituting a default for a null operand.** To treat null cells in either of two maps as if they held the value 1 before multiplying them — rather than letting either map's null poison the whole product — the ''?'' fallback is the most concise form: | ||
| + | |||
| + | <code> | ||
| + | (i1 ? 1) * (i2 ? 1) | ||
| + | </code> | ||
| + | |||
| + | The equivalent using ''isNull'' is more verbose but reads the same way: | ||
| + | |||
| + | <code> | ||
| + | (if isNull(i1) then 1 else i1) * (if isNull(i2) then 1 else i2) | ||
| + | </code> | ||
| + | |||
| + | Both are shorthand for the fully exhaustive case analysis, spelled out explicitly: | ||
| + | |||
| + | <code> | ||
| + | if isNull(i1) and isNull(i2) then 1 | ||
| + | else if not isNull(i1) and not isNull(i2) then i1 * i2 | ||
| + | else if not isNull(i1) and isNull(i2) then i1 | ||
| + | else i2 | ||
| + | </code> | ||
| + | |||
| + | The exhaustive form is only worth writing out when each of the four combinations genuinely needs different treatment; when they don't, as here, the shorter forms above are equivalent and preferable. | ||
| If the computed real value exceeds the range of the chosen Cell Type, that cell is also written as null rather than wrapping or clipping. This range check applies to [[Calculate Map]] and [[Calculate Categorical Map]] only — the only two functors that accept a Cell Type parameter and write to a typed cell grid. | If the computed real value exceeds the range of the chosen Cell Type, that cell is also written as null rather than wrapping or clipping. This range check applies to [[Calculate Map]] and [[Calculate Categorical Map]] only — the only two functors that accept a Cell Type parameter and write to a typed cell grid. | ||
| + | |||
| + | ===== When a conditional's test needs guarding against null ===== | ||
| + | |||
| + | A conditional's test poisoning the whole result is only a problem when the tested operand's nullness shouldn't determine whether the entire expression is invalid. Reclassifying ''i1'' in place — ''if i1 = 2 then 10 else i1'' — needs no explicit guard, since a null ''i1'' making the reclassified result null for that cell is exactly the desired behaviour: the expression is internally equivalent to ''if isNull(i1) then null else if i1 = 2 then 10 else i1''. | ||
| + | |||
| + | The same mechanism causes a real bug when the operand being tested isn't the one the null-ness should actually be judged against. Combining a landscape map (''i1'') with a road map (''i2'', where 1 marks a road and null marks everything else) might suggest ''if i2 = 1 then 1 else i1'' — but this is internally equivalent to ''if isNull(i2) then null else if i2 = 1 then 1 else i1''. Since most cells are null in a sparse road map, most of the output becomes null too, discarding ''i1'''s landscape values everywhere a road isn't present — even though ''i2'' being null says nothing about whether ''i1'''s value is valid. | ||
| + | |||
| + | Guarding the test with ''isNull'' fixes it: ''if not isNull(i2) and i2 = 1 then 1 else i1'' only tests ''i2'' once it's confirmed non-null, so a null road cell falls through to the landscape value in ''i1'' instead of nulling the result. | ||
| ===== Exceptions in map calculators ===== | ===== Exceptions in map calculators ===== | ||
| Line 891: | Line 932: | ||
| | Calculate Lookup Table Values | ''CalculateLookupTableValues'' | | | Calculate Lookup Table Values | ''CalculateLookupTableValues'' | | ||
| | Calculate Lookup Table Keys and Values | ''CalculateLookupTableKeysAndValues'' | | | Calculate Lookup Table Keys and Values | ''CalculateLookupTableKeysAndValues'' | | ||
| - | |||
| - | |||