From 2202a659746792024cdf31ca0ee4588a65799d9d Mon Sep 17 00:00:00 2001 From: "K.Tolstygin" Date: Mon, 24 Aug 2026 16:28:08 +0300 Subject: [PATCH 1/3] feat(spec): add categorical inputs and record lookups Add reusable named input definitions with optional enum types, so a function can bind a categorical input by name while retaining its own symbol, unit, domain, and description. Keep enum definitions focused on their stable members and canonical values rather than duplicating binding-specific metadata. Add typed record lookups keyed by enum inputs. Validate that a lookup references an in-scope categorical input, covers every enum member exactly once, and provides exactly the fields required by its declared record output. Compile lookups into the semantic IR as explicit conversions from categorical values to numeric record fields. Lower categorical inputs and lookups consistently across Rust, C, C++, and Python. Generate target-native enum types and lookup expressions, namespace C enum identifiers by enum type, and retain the existing public APIs and documentation contracts. Extend the NumPy extension and Python wrappers with uint32-backed enum arrays, broadcasting, scalar dispatch, typing, generated enum documentation, and tests. Co-authored-by: Petr Tsymbarovich --- .agents/skills/ptf-extract/SKILL.md | 5 + .../references/categorical-lookups.md | 86 ++++ .../references/extraction-quality-gate.md | 8 + .agents/skills/ptf-generate/SKILL.md | 9 +- .../references/generation-checklist.md | 13 +- .agents/skills/ptf-review/SKILL.md | 13 +- .../implementation-review-checklist.md | 26 +- codegen/src/compile.rs | 53 +- codegen/src/documentation.rs | 84 ++- codegen/src/formula.pest | 4 +- codegen/src/formula.rs | 27 + codegen/src/model.rs | 423 ++++++++++++++- codegen/src/render/c.rs | 41 +- codegen/src/semantic.rs | 484 ++++++++++-------- codegen/src/specs.rs | 315 ++++++++++-- codegen/src/targets/catalog.rs | 100 ++-- codegen/src/targets/mod.rs | 63 ++- codegen/src/targets/native.rs | 376 ++++++++++++-- codegen/src/targets/python/extension.rs | 48 +- codegen/src/targets/python/test.rs | 61 ++- codegen/src/targets/python/wrapper.rs | 264 +++++++++- codegen/src/targets/reference/c.rs | 86 +++- codegen/src/targets/reference/cpp.rs | 61 ++- codegen/src/targets/rust.rs | 232 ++++++++- codegen/src/validate.rs | 66 ++- docs/src/ptf-catalog/index.md | 37 +- docs/src/ptf-catalog/sources/ahuja1984.md | 12 +- docs/src/ptf-catalog/sources/aimrun2009.md | 12 +- docs/src/ptf-catalog/sources/beniaich2023.md | 120 ++--- .../ptf-catalog/sources/chakraborty2011.md | 62 +-- docs/src/ptf-catalog/sources/cosby1984.md | 10 +- .../ptf-catalog/sources/dharumarajan2019.md | 42 +- .../ptf-catalog/sources/ferrerjulia2004.md | 324 ++++++------ docs/src/ptf-catalog/sources/hodnett2002.md | 18 +- docs/src/ptf-catalog/sources/jabro1992.md | 10 +- docs/src/ptf-catalog/sources/li2007.md | 14 +- docs/src/ptf-catalog/sources/mayr1999.md | 14 +- .../src/ptf-catalog/sources/oosterveld1980.md | 48 +- docs/src/ptf-catalog/sources/pidgeon1972.md | 130 ++--- docs/src/ptf-catalog/sources/puckett1985.md | 14 +- docs/src/ptf-catalog/sources/rawls1982.md | 32 +- docs/src/ptf-catalog/sources/saxton2006.md | 78 +-- docs/src/ptf-catalog/sources/tiwary2014.md | 26 +- docs/src/ptf-catalog/sources/varallyai1982.md | 34 +- docs/src/ptf-catalog/sources/vereecken1989.md | 42 +- docs/src/ptf-catalog/sources/wang2012.md | 16 +- docs/src/ptf-catalog/sources/weber2020.md | 16 +- specs/schema/ptf-spec.schema.json | 107 +++- targets/ptfkit-native/cpp/CMakeLists.txt | 2 +- .../include/ptfkit/detail/record.h | 12 + targets/ptfkit-py/src/ptfkit/_dispatch.py | 4 +- targets/ptfkit-py/src/ptfkit/ahuja1984.c | 5 +- targets/ptfkit-py/src/ptfkit/aimrun2009.c | 5 +- targets/ptfkit-py/src/ptfkit/beniaich2023.c | 66 ++- .../ptfkit-py/src/ptfkit/chakraborty2011.c | 37 +- targets/ptfkit-py/src/ptfkit/cosby1984.c | 6 +- .../ptfkit-py/src/ptfkit/dharumarajan2019.c | 23 +- targets/ptfkit-py/src/ptfkit/enums.py | 61 +++ .../ptfkit-py/src/ptfkit/ferrerjulia2004.c | 179 +++++-- targets/ptfkit-py/src/ptfkit/hodnett2002.c | 6 +- targets/ptfkit-py/src/ptfkit/jabro1992.c | 4 +- targets/ptfkit-py/src/ptfkit/li2007.c | 6 +- targets/ptfkit-py/src/ptfkit/mayr1999.c | 5 +- targets/ptfkit-py/src/ptfkit/oosterveld1980.c | 24 +- targets/ptfkit-py/src/ptfkit/pidgeon1972.c | 80 ++- targets/ptfkit-py/src/ptfkit/puckett1985.c | 6 +- targets/ptfkit-py/src/ptfkit/rawls1982.c | 18 +- targets/ptfkit-py/src/ptfkit/saxton2006.c | 39 +- targets/ptfkit-py/src/ptfkit/tiwary2014.c | 12 +- targets/ptfkit-py/src/ptfkit/ufunc.h | 10 +- targets/ptfkit-py/src/ptfkit/varallyai1982.c | 20 +- targets/ptfkit-py/src/ptfkit/vereecken1989.c | 12 +- targets/ptfkit-py/src/ptfkit/wang2012.c | 6 +- targets/ptfkit-py/src/ptfkit/weber2020.c | 6 +- targets/ptfkit-py/tests/_helpers.py | 19 +- 75 files changed, 3679 insertions(+), 1160 deletions(-) create mode 100644 .agents/skills/ptf-extract/references/categorical-lookups.md create mode 100644 targets/ptfkit-native/include/ptfkit/detail/record.h create mode 100644 targets/ptfkit-py/src/ptfkit/enums.py diff --git a/.agents/skills/ptf-extract/SKILL.md b/.agents/skills/ptf-extract/SKILL.md index 0499c79..a3be592 100644 --- a/.agents/skills/ptf-extract/SKILL.md +++ b/.agents/skills/ptf-extract/SKILL.md @@ -19,6 +19,8 @@ input error. 1. Read the supplied local paper, `specs/schema/ptf-spec.schema.json`, and `references/extraction-quality-gate.md`. + If the source defines a finite categorical input or a table selected by that + input, also read `references/categorical-lookups.md`. 2. Extract only facts explicitly supported by the paper. Write its standalone YAML directly to `specs/functions/.yaml`, following `references/spec-template.yaml`. @@ -42,6 +44,9 @@ exact YAML path and explicit blockers. - Do not set `implemented`, run target generation, or edit generated files. - Do not invent formulas, units, metadata, golden values, applicability, or API details. Keep uncertainty explicit in the YAML. +- Do not normalize, alias, abbreviate, or otherwise broaden source-defined + categorical values. Keep enum member names, canonical textual values, lookup + rows, and their evidence distinct. - Give every `type: record` output a PascalCase `name`, whether it is inline or declared in `$defs`. It names generated structures and classes; `$defs` keys only resolve local `$ref` targets. diff --git a/.agents/skills/ptf-extract/references/categorical-lookups.md b/.agents/skills/ptf-extract/references/categorical-lookups.md new file mode 100644 index 0000000..c501f2a --- /dev/null +++ b/.agents/skills/ptf-extract/references/categorical-lookups.md @@ -0,0 +1,86 @@ +# Categorical inputs and typed lookups + +Use this contract only when the source explicitly defines a finite categorical +input or a complete numeric table selected by such an input. Do not replace a +continuous predictor with representative categories, derive categories from +numeric inputs, or add aliases and normalization that the source does not define. + +## Enum type and input binding + +Declare the reusable categorical type in `$defs`. Its key is the canonical +PascalCase type name. The required `description` documents the type as a whole. +Each value has a lower-snake-case schema `name`, the exact public textual +`value`, and an optional source-supported `description`. + +Bind the type to a function argument with `name` and `$ref`. Add the optional +binding `description` only when the argument's role needs information beyond +the enum type description. Categorical inputs do not have units, symbols, or +numeric domains. + +```yaml +$defs: + TextureClass: + type: enum + description: Source-defined texture class. + values: + - name: coarse + value: "Coarse" + description: Source-defined coarse class. +functions: + - inputs: + - name: texture + description: Class used to select the published table row. + $ref: "#/$defs/TextureClass" +``` + +## Lookup definition and implementation + +A lookup definition references an enum input type and a record output type. +Its rows must cover every enum member exactly once. Row `key` values are enum +member schema names; every row `value` must contain exactly the output record's +field names. + +Invoke the lookup as an ordered implementation variable. Its `key` names an +in-scope input of the lookup's enum type. Return a compatible record-valued +variable directly, or use `variable.field` in later formula expressions. +Golden-test categorical inputs also use enum member schema names. + +```yaml +$defs: + Parameters: + type: record + name: Parameters + fields: + - name: coefficient + symbol: c + unit: "1" + domain: null + description: Published coefficient. + ParametersByTexture: + type: lookup + input: + $ref: "#/$defs/TextureClass" + output: + $ref: "#/$defs/Parameters" + values: + - key: coarse + value: {coefficient: 1.25} +functions: + - implementation: + variables: + - name: parameters + lookup: + table: + $ref: "#/$defs/ParametersByTexture" + key: texture + golden_tests: + - id: coarse_table_row + inputs: {texture: coarse} + expected: {coefficient: 1.25} + rtol: 0.0 + atol: 0.0 + notes: Direct published table row. +``` + +Treat a missing category, ambiguous label, incomplete row, unexplained numeric +value, or uncertain category-to-row mapping as a scientific blocker. diff --git a/.agents/skills/ptf-extract/references/extraction-quality-gate.md b/.agents/skills/ptf-extract/references/extraction-quality-gate.md index 4066b50..4d4bc5a 100644 --- a/.agents/skills/ptf-extract/references/extraction-quality-gate.md +++ b/.agents/skills/ptf-extract/references/extraction-quality-gate.md @@ -11,6 +11,14 @@ APA-style slug and identifies the generated public module, for example golden and edge cases, documentation, scope, and semantic `implementation` fields required by the schema. - Every record output has an explicit PascalCase `name`. +- When the source uses a finite categorical predictor, represent its reusable + type as an enum in `$defs` and bind it to each function-local argument with + `name` plus `$ref`. The enum owns its type description and admissible values; + the binding description, when present, explains only that argument's role. +- When the source publishes a table selected by a category, model it as a typed + lookup from the enum to a record in `$defs`. Preserve one row per enum member + and one numeric value per output-record field. Use enum member names in lookup + keys and golden inputs, not canonical textual values or target ordinals. - Use the formula DSL only in `implementation` expressions. In `scientific_notes`, retain source-supported scientific context, derivations needed to justify an interpretation, evidence for review decisions, citations, diff --git a/.agents/skills/ptf-generate/SKILL.md b/.agents/skills/ptf-generate/SKILL.md index 2423cc7..1c87bf1 100644 --- a/.agents/skills/ptf-generate/SKILL.md +++ b/.agents/skills/ptf-generate/SKILL.md @@ -1,6 +1,6 @@ --- name: ptf-generate -description: Generate and verify ptfkit Rust and NumPy targets for one reviewed APA-style source slug. Use after human review of a YAML source file in specs/functions to validate, generate, test, prove idempotence, and atomically mark the source implemented. +description: Generate and verify all retained ptfkit targets for one reviewed APA-style source slug. Use after human review of a YAML source file in specs/functions to validate, generate, test, prove idempotence, and atomically mark the source implemented. --- # PTF Generate @@ -23,8 +23,11 @@ status when input validation fails; report the blocking input error. 2. Reject unresolved blockers, `TODO` values, schema or semantic failures, and output-metadata mismatches. Record `outputs.name` is PascalCase and names generated structures and classes; `$defs` keys only resolve local references. - Do not infer missing science. -3. Validate, generate both retained targets, and run the required verification + For categorical inputs and lookups, verify the named enum binding, exact + member names and canonical values, complete enum-to-record mapping, lookup + key type, row fields, and categorical golden inputs. Do not infer missing + science. +3. Validate, generate all retained targets, and run the required verification gates. Before validation, extract each repeated nontrivial formula expression within a function into one earlier local implementation variable and reference it thereafter; retain published numeric lexemes and do not diff --git a/.agents/skills/ptf-generate/references/generation-checklist.md b/.agents/skills/ptf-generate/references/generation-checklist.md index b0fb39a..7e1276b 100644 --- a/.agents/skills/ptf-generate/references/generation-checklist.md +++ b/.agents/skills/ptf-generate/references/generation-checklist.md @@ -10,6 +10,13 @@ matching ordered output metadata. - Every record `outputs.name` is PascalCase and names generated structures and classes. +- Every categorical input binds a function-local `name` to an enum `$ref`. + Enum member names and canonical textual values are unique, and categorical + golden inputs name enum members rather than textual values or ordinals. +- Every lookup references an enum input and record output, covers every enum + member exactly once, and gives every row exactly the record's fields. Each + lookup invocation uses an in-scope key of the declared enum type; field + access and direct record return match the resolved record type. - Run `cargo run --manifest-path codegen/Cargo.toml -- validate` before generation. @@ -39,7 +46,9 @@ second generation idempotence check. Investigate every unexpected diff. - Invalid, incomplete, or ambiguous science: return a spec blocker for the user to resolve. -- Valid semantic IR unsupported by Rust or the native NumPy backend: return a - generator capability blocker. Do not hand-write a retained target. +- Valid semantic IR unsupported by any retained Rust, C, C++, or Python path: + return a generator capability blocker. This includes categorical types, typed + lookups, and record-field access. Do not hand-write a retained computational + target. - A manual public module may only wrap its generated native ufunc; it does not disable native generation or allow a duplicate formula. diff --git a/.agents/skills/ptf-review/SKILL.md b/.agents/skills/ptf-review/SKILL.md index 40576e5..7296227 100644 --- a/.agents/skills/ptf-review/SKILL.md +++ b/.agents/skills/ptf-review/SKILL.md @@ -1,6 +1,6 @@ --- name: ptf-review -description: Independently review a generated ptfkit PTF source against its YAML specification, semantic IR, retained Rust and NumPy targets, generation policy, and public API parity. Use for read-only pre-merge review after $ptf-generate. +description: Independently review a generated ptfkit PTF source against its YAML specification, semantic IR, retained targets, generation policy, and public API parity. Use for read-only pre-merge review after $ptf-generate. --- # PTF Review @@ -18,12 +18,13 @@ modifying repository state. ## Procedure 1. Read `specs/schema/ptf-spec.schema.json`, the selected YAML specification, - implementation diff, generated Rust and native NumPy targets, golden tests, - and relevant public wrapper. + implementation diff, generated Rust, C, C++, and native NumPy targets, + golden tests, and relevant public wrapper. 2. Load `references/implementation-review-checklist.md`. -3. Check schema and semantic IR fidelity, both retained targets, deterministic - regeneration, status transition evidence, output order, NumPy broadcasting, - `out`, `NamedTuple` compatibility, docstring fidelity, and public API parity. +3. Check schema and semantic IR fidelity, all retained targets, deterministic + regeneration, status transition evidence, categorical type and lookup + fidelity, output order, NumPy broadcasting, `out`, enum-array typing, + `NamedTuple` compatibility, docstring fidelity, and public API parity. 4. Run or request the project checks appropriate to the changed files. 5. Report findings first, ordered by severity, with file and line references. diff --git a/.agents/skills/ptf-review/references/implementation-review-checklist.md b/.agents/skills/ptf-review/references/implementation-review-checklist.md index 314df9e..757d825 100644 --- a/.agents/skills/ptf-review/references/implementation-review-checklist.md +++ b/.agents/skills/ptf-review/references/implementation-review-checklist.md @@ -7,6 +7,15 @@ and classes; `$defs` keys only resolve local references. - [ ] Every public function name, argument, output, unit, and IR expression matches the YAML specification. +- [ ] Every categorical argument binds its function-local name to the intended + enum type; type and optional binding descriptions retain their separate roles. +- [ ] Enum member names, exact canonical textual values, order, and optional + descriptions match the source-supported specification. Golden inputs use + member names, never textual values or generated ordinals. +- [ ] Every lookup maps the declared enum to the declared record, covers each + member exactly once, and gives each row exactly the record fields. Lookup + invocation keys have the declared enum type, and record-field access resolves + to real fields. - [ ] No scientific assumption is present only in generated code. ## Formula and units @@ -16,8 +25,10 @@ ## Retained targets -- [ ] Generated Rust uses `f64` scalar computation from the semantic IR. -- [ ] Generated native NumPy ufuncs use the same IR. +- [ ] Generated Rust, C, and C++ preserve enum types, member identity, typed + lookup conversion, record shape, and numeric computation from the semantic IR. +- [ ] Generated native NumPy ufuncs use the same IR and private ordinal encoding + only as a target implementation detail. - [ ] Generated target tests cover every structured golden case. - [ ] Valid IR unsupported by a retained target is reported as a generator capability blocker, not replaced with hand-written computation. @@ -26,6 +37,11 @@ - [ ] Public module and function names, keyword-only inputs, scalar/array behavior, broadcasting, `out`, and `NamedTuple` output match the contract. +- [ ] Python exposes scalar categorical inputs as the generated enum and array + inputs as its typed `EnumArray`; raw strings, integers, arbitrary arrays, and + normalization aliases are not silently accepted. +- [ ] Generated enum type and member documentation reflects the enum and member + descriptions without conflating them with a function binding description. - [ ] A manual public module is justified and delegates to generated native ufuncs without duplicating formulas. @@ -40,6 +56,6 @@ ## Blocking findings Classify as blocking: schema or semantic failure; formula, unit, output-order, -or public-API mismatch; missing retained target or golden test; unsupported IR; -nondeterministic generation; unsubstantiated status transition; or exposed -repository-only specification paths. +categorical-type, lookup, or public-API mismatch; missing retained target or +golden test; unsupported IR; nondeterministic generation; unsubstantiated +status transition; or exposed repository-only specification paths. diff --git a/codegen/src/compile.rs b/codegen/src/compile.rs index 170fb48..dd3b1a7 100644 --- a/codegen/src/compile.rs +++ b/codegen/src/compile.rs @@ -1,7 +1,8 @@ use anyhow::{Context, Result}; use crate::model::{ - CompiledFunction, CompiledGoldenTest, CoreFunction, Entry, Function, Output, Outputs, + CompiledFunction, CompiledGoldenTest, CompiledInput, CoreFunction, Entry, Function, + GoldenInput, Output, Outputs, }; pub(super) fn functions(entries: Vec) -> Result> { @@ -28,12 +29,12 @@ pub(super) fn functions(entries: Vec) -> Result> { inputs: function .inputs .iter() - .map(|input| input.name.clone()) + .map(|input| input.name().to_owned()) .collect(), output, }; compiled.push(CompiledFunction { - golden_tests: golden_tests(function, &core)?, + golden_tests: golden_tests(function)?, core, entry: entry.clone(), function_index, @@ -44,18 +45,52 @@ pub(super) fn functions(entries: Vec) -> Result> { Ok(compiled) } -fn golden_tests(function: &Function, core: &CoreFunction) -> Result> { +fn golden_tests(function: &Function) -> Result> { function .golden_tests .iter() .map(|case| { - let inputs = core + let inputs = function .inputs .iter() - .map(|name| { - case.inputs.get(name).copied().with_context(|| { - format!("golden test `{}` is missing input `{name}`", case.id) - }) + .map(|input| { + let input_name = input.name(); + let value = case.inputs.get(input_name).with_context(|| { + format!( + "golden test `{}` is missing input `{}`", + case.id, input_name + ) + })?; + match (input.enum_type(), value) { + (None, GoldenInput::Number(value)) => Ok(CompiledInput::Number(*value)), + (Some(enum_type), GoldenInput::Enum(member_name)) => { + enum_type + .values + .iter() + .find(|member| member.name == *member_name) + .with_context(|| { + format!( + "golden test `{}` input `{}` references unknown member `{member_name}` of enum `{}`", + case.id, input_name, enum_type.name + ) + })?; + Ok(CompiledInput::Enum { + enum_name: enum_type.name.clone(), + member_name: member_name.clone(), + }) + } + (None, GoldenInput::Enum(_)) => anyhow::bail!( + "golden test `{}` input `{}` must be numeric", + case.id, + input_name + ), + (Some(enum_type), GoldenInput::Number(_)) => anyhow::bail!( + "golden test `{}` input `{}` must name a member of enum `{}`", + case.id, + input_name, + enum_type.name + ), + } }) .collect::>>()?; let expected = function diff --git a/codegen/src/documentation.rs b/codegen/src/documentation.rs index e912483..3bd9935 100644 --- a/codegen/src/documentation.rs +++ b/codegen/src/documentation.rs @@ -3,7 +3,7 @@ //! Targets choose their own section ordering and markup. This module only //! describes the information they have available to render. -use crate::model::{Function, Outputs, Parameter, Scope, Source}; +use crate::model::{Function, Input, Outputs, Parameter, Scope, Source}; #[derive(Clone, Copy, Debug)] pub(crate) struct SourceDocument<'a> { @@ -28,7 +28,7 @@ pub(crate) struct Doi<'a> { #[derive(Clone, Copy, Debug)] pub(crate) struct FunctionDocument<'a> { pub(crate) summary: &'a str, - pub(crate) parameters: &'a [Parameter], + pub(crate) parameters: &'a [Input], pub(crate) returns: Returns<'a>, pub(crate) territory: Option<&'a str>, pub(crate) models: Models<'a>, @@ -93,21 +93,70 @@ pub(crate) fn for_function(function: &Function) -> FunctionDocument<'_> { } } -pub(crate) fn parameter_details(parameter: &Parameter) -> String { - format!("{} ({})", parameter.description, parameter.unit) +pub(crate) trait ParameterMetadata { + fn name(&self) -> &str; + fn description(&self) -> &str; + fn unit(&self) -> Option<&str>; } -pub(crate) fn parameter_documentation(parameter: &Parameter) -> String { - format!("{}: {}", parameter.name, parameter_details(parameter)) +impl ParameterMetadata for Parameter { + fn name(&self) -> &str { + &self.name + } + fn description(&self) -> &str { + &self.description + } + fn unit(&self) -> Option<&str> { + Some(&self.unit) + } +} + +impl ParameterMetadata for Input { + fn name(&self) -> &str { + self.name() + } + fn description(&self) -> &str { + self.description() + } + fn unit(&self) -> Option<&str> { + self.unit() + } +} + +pub(crate) fn parameter_details(parameter: &impl ParameterMetadata) -> String { + match parameter.unit() { + Some(unit) => format!("{} ({unit})", parameter.description()), + None => parameter.description().to_owned(), + } +} + +pub(crate) fn parameter_documentation(parameter: &impl ParameterMetadata) -> String { + format!("{}: {}", parameter.name(), parameter_details(parameter)) } #[cfg(test)] mod tests { use crate::model::{ - Documentation, Function, FunctionScope, Models, Outputs, Parameter, PublicApi, + Documentation, Function, FunctionScope, Input, Models, Outputs, Parameter, PublicApi, }; - use super::{Returns, for_function}; + use super::{ParameterMetadata, Returns, for_function, parameter_details}; + + struct EnumInputMetadata; + + impl ParameterMetadata for EnumInputMetadata { + fn name(&self) -> &str { + "soil_texture" + } + + fn description(&self) -> &str { + "USDA soil textural class." + } + + fn unit(&self) -> Option<&str> { + None + } + } fn parameter(name: &str) -> Parameter { Parameter { @@ -118,6 +167,23 @@ mod tests { } } + fn input(name: &str) -> Input { + Input::Parameter(Parameter { + name: name.into(), + unit: "cm^3/cm^3".into(), + domain: None, + description: format!("{name} description."), + }) + } + + #[test] + fn omits_a_unit_suffix_for_enum_input_metadata() { + assert_eq!( + parameter_details(&EnumInputMetadata), + "USDA soil textural class." + ); + } + #[test] fn preserves_empty_optional_documentation_sections() { let function = Function { @@ -169,7 +235,7 @@ mod tests { k_h: Some("Conductivity model.".into()), }, }, - inputs: vec![parameter("sand")], + inputs: vec![input("sand")], outputs: Outputs::Record { name: "TestResult".into(), fields: vec![parameter("theta_33"), parameter("theta_1500")], diff --git a/codegen/src/formula.pest b/codegen/src/formula.pest index 3e32330..4caade9 100644 --- a/codegen/src/formula.pest +++ b/codegen/src/formula.pest @@ -6,9 +6,10 @@ addition = { multiplication ~ ((plus | minus) ~ multiplication)* } multiplication = { unary ~ ((times | divide) ~ unary)* } unary = { (plus | minus)* ~ power } power = { primary ~ (power_operator ~ unary)? } -primary = { number | call | identifier | parenthesized } +primary = { number | call | field | identifier | parenthesized } parenthesized = { lparen ~ expression ~ rparen } call = { identifier ~ lparen ~ (expression ~ (comma ~ expression)*)? ~ rparen } +field = { identifier ~ dot ~ identifier } number = @{ (ASCII_DIGIT+ ~ ("." ~ ASCII_DIGIT*)? | "." ~ ASCII_DIGIT+) ~ (("e" | "E") ~ (plus | minus)? ~ ASCII_DIGIT+)? } identifier = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHANUMERIC | "_")* } @@ -19,5 +20,6 @@ times = { "*" } divide = { "/" } power_operator = { "^" } comma = { "," } +dot = { "." } lparen = { "(" } rparen = { ")" } diff --git a/codegen/src/formula.rs b/codegen/src/formula.rs index 9eb3cd1..ce5d21b 100644 --- a/codegen/src/formula.rs +++ b/codegen/src/formula.rs @@ -29,6 +29,10 @@ pub(crate) struct Number { pub(crate) enum ExprKind { Number(Number), Variable(String), + Field { + base: String, + field: String, + }, Unary { op: UnaryOp, operand: Box, @@ -130,6 +134,7 @@ fn expected_token(rule: &Rule) -> Option<&'static str> { Rule::lparen => Some("`(`"), Rule::rparen => Some("`)`"), Rule::comma => Some("`,`"), + Rule::dot => Some("`.`"), Rule::plus => Some("`+`"), Rule::minus => Some("`-`"), Rule::times => Some("`*`"), @@ -178,6 +183,27 @@ fn build_expression(pair: Pair<'_, Rule>, location: &str) -> Result { + let expression_span = span(&pair); + let mut identifiers = pair + .into_inner() + .filter(|child| child.as_rule() == Rule::identifier); + Ok(Expr { + kind: ExprKind::Field { + base: identifiers + .next() + .expect("field has a base") + .as_str() + .to_owned(), + field: identifiers + .next() + .expect("field has a member") + .as_str() + .to_owned(), + }, + span: expression_span, + }) + } Rule::call => build_call(pair, location), Rule::parenthesized => { let span = span(&pair); @@ -317,6 +343,7 @@ mod tests { match &expression.kind { ExprKind::Number(number) => format!("{}", number.value), ExprKind::Variable(name) => name.clone(), + ExprKind::Field { base, field } => format!("{base}.{field}"), ExprKind::Unary { op, operand } => match op { UnaryOp::Plus => format!("(+ {})", shape(operand)), UnaryOp::Minus => format!("(- {})", shape(operand)), diff --git a/codegen/src/model.rs b/codegen/src/model.rs index 2939d8e..c5d8213 100644 --- a/codegen/src/model.rs +++ b/codegen/src/model.rs @@ -27,8 +27,10 @@ struct RawSpec { #[derive(Clone, Debug, Deserialize)] #[serde(untagged)] enum Definition { - Input(Parameter), + Enum(EnumDefinition), + Lookup(LookupDefinition), Output(Outputs), + Parameter(Parameter), } #[derive(Clone, Debug, Deserialize)] @@ -49,10 +51,19 @@ struct FunctionReference { #[derive(Clone, Debug, Deserialize)] #[serde(untagged)] enum InputReference { - Inline(Parameter), + Parameter(Parameter), + Type(NamedReference), Reference(Reference), } +#[derive(Clone, Debug, Deserialize)] +struct NamedReference { + name: String, + description: Option, + #[serde(flatten)] + reference: Reference, +} + #[derive(Clone, Debug, Deserialize)] #[serde(untagged)] enum OutputReference { @@ -60,8 +71,8 @@ enum OutputReference { Reference(Reference), } -#[derive(Clone, Debug, Deserialize)] -struct Reference { +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct Reference { #[serde(rename = "$ref")] target: String, } @@ -72,6 +83,18 @@ impl<'de> Deserialize<'de> for Spec { D: Deserializer<'de>, { let raw = RawSpec::deserialize(deserializer)?; + let lookup_definitions = raw + .definitions + .iter() + .filter_map(|(name, definition)| match definition { + Definition::Lookup(definition) => Some( + resolve_lookup_definition(name, definition, &raw.definitions) + .map(|definition| (name.clone(), definition)), + ), + _ => None, + }) + .collect::, _>>() + .map_err(serde::de::Error::custom)?; let functions = raw .functions .into_iter() @@ -80,11 +103,24 @@ impl<'de> Deserialize<'de> for Spec { .inputs .into_iter() .map(|input| match input { - InputReference::Inline(input) => Ok(input), + InputReference::Parameter(parameter) => Ok(Input::Parameter(parameter)), + InputReference::Type(reference) => resolve_input_type( + reference, + &raw.definitions, + &function.name, + ), InputReference::Reference(reference) => { let name = definition_name(&reference.target)?; match raw.definitions.get(name) { - Some(Definition::Input(input)) => Ok(input.clone()), + Some(Definition::Parameter(parameter)) => { + Ok(Input::Parameter(parameter.clone())) + } + Some(Definition::Enum(_)) | Some(Definition::Lookup(_)) => { + Err(format!( + "function {} must bind a name when referencing type definition `{name}` as an input", + function.name + )) + } Some(Definition::Output(_)) => Err(format!( "function {} references output definition `{name}` as an input", function.name @@ -102,8 +138,12 @@ impl<'de> Deserialize<'de> for Spec { OutputReference::Reference(reference) => { let name = definition_name(&reference.target)?; match raw.definitions.get(name) { - Some(Definition::Input(_)) => Err(format!( - "function {} references input definition `{name}` as an output", + Some(Definition::Enum(_)) | Some(Definition::Lookup(_)) => Err(format!( + "function {} references non-output definition `{name}` as an output", + function.name + )), + Some(Definition::Parameter(_)) => Err(format!( + "function {} references parameter definition `{name}` as an output", function.name )), Some(Definition::Output(outputs)) => Ok(outputs.clone()), @@ -114,6 +154,25 @@ impl<'de> Deserialize<'de> for Spec { } } }?; + let implementation = function + .implementation + .map(|mut implementation| { + for variable in &mut implementation.variables { + if let ImplementationVariable::Lookup { lookup, .. } = variable { + let name = definition_name(&lookup.table.target)?; + lookup.definition = Some( + lookup_definitions.get(name).cloned().ok_or_else(|| { + format!( + "function {} references unknown lookup definition `{name}`", + function.name + ) + })?, + ); + } + } + Ok::<_, String>(implementation) + }) + .transpose()?; Ok(Function { name: function.name, status: function.status, @@ -121,7 +180,7 @@ impl<'de> Deserialize<'de> for Spec { scope: function.scope, inputs, outputs, - implementation: function.implementation, + implementation, golden_tests: function.golden_tests, documentation: function.documentation, }) @@ -137,6 +196,134 @@ impl<'de> Deserialize<'de> for Spec { } } +fn resolve_input_type( + input: NamedReference, + definitions: &BTreeMap, + function: &str, +) -> Result { + let type_name = definition_name(&input.reference.target)?; + match definitions.get(type_name) { + Some(Definition::Enum(definition)) => { + let mut definition = definition.clone(); + definition.name = type_name.to_owned(); + Ok(Input::Enum { + name: input.name, + description: input.description, + definition, + }) + } + Some(Definition::Output(_)) + | Some(Definition::Lookup(_)) + | Some(Definition::Parameter(_)) => Err(format!( + "function {function} input {} references non-enum definition `{type_name}` as its type", + input.name + )), + None => Err(format!( + "function {function} input {} references unknown enum definition `{type_name}`", + input.name + )), + } +} + +fn resolve_lookup_definition( + name: &str, + definition: &LookupDefinition, + definitions: &BTreeMap, +) -> Result { + let input_name = definition_name(&definition.input.target)?; + let input_type = match definitions.get(input_name) { + Some(Definition::Enum(definition)) => { + let mut definition = definition.clone(); + definition.name = input_name.to_owned(); + definition + } + Some(_) => { + return Err(format!( + "lookup `{name}` input must reference an enum definition" + )); + } + None => { + return Err(format!( + "lookup `{name}` references unknown input type `{input_name}`" + )); + } + }; + let output_name = definition_name(&definition.output.target)?; + let output_type = match definitions.get(output_name) { + Some(Definition::Output(output @ Outputs::Record { .. })) => output.clone(), + Some(_) => { + return Err(format!( + "lookup `{name}` output must reference a record definition" + )); + } + None => { + return Err(format!( + "lookup `{name}` references unknown output type `{output_name}`" + )); + } + }; + let mut resolved = definition.clone(); + resolved.name = name.to_owned(); + resolved.input_type = Some(input_type); + resolved.output_type = Some(output_type); + validate_lookup_values(&resolved)?; + Ok(resolved) +} + +fn validate_lookup_values(lookup: &LookupDefinition) -> Result<(), String> { + let enum_type = lookup + .input_type + .as_ref() + .expect("lookup input type is resolved"); + let output = lookup + .output_type + .as_ref() + .expect("lookup output type is resolved"); + let output_names = output + .fields() + .iter() + .map(|field| field.name.as_str()) + .collect::>(); + let mut keys = std::collections::BTreeSet::new(); + for (index, row) in lookup.values.iter().enumerate() { + if !keys.insert(row.key.as_str()) { + return Err(format!( + "lookup `{}` has duplicate key `{}` at value {index}", + lookup.name, row.key + )); + } + if !enum_type.values.iter().any(|member| member.name == row.key) { + return Err(format!( + "lookup `{}` has unknown enum member `{}` at value {index}", + lookup.name, row.key + )); + } + let row_names = row + .value + .keys() + .map(String::as_str) + .collect::>(); + if row_names != output_names { + return Err(format!( + "lookup `{}` value {index} keys must exactly match output fields", + lookup.name + )); + } + } + let enum_members = enum_type + .values + .iter() + .map(|member| member.name.as_str()) + .collect::>(); + if keys != enum_members { + return Err(format!( + "lookup `{}` values must cover every member of enum `{}` exactly once", + lookup.name, enum_type.name + )); + } + Ok(()) +} + fn definition_name(reference: &str) -> Result<&str, String> { let Some(name) = reference.strip_prefix("#/$defs/") else { return Err(format!( @@ -170,13 +357,13 @@ pub(crate) struct Scope { pub(crate) dataset: Option, } -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Debug, Serialize)] pub(crate) struct Function { pub(crate) name: String, pub(crate) status: String, pub(crate) public_api: PublicApi, pub(crate) scope: FunctionScope, - pub(crate) inputs: Vec, + pub(crate) inputs: Vec, pub(crate) outputs: Outputs, pub(crate) implementation: Option, #[serde(default)] @@ -197,12 +384,19 @@ impl Function { #[derive(Clone, Debug, Deserialize, Serialize)] pub(crate) struct GoldenTest { pub(crate) id: String, - pub(crate) inputs: BTreeMap, + pub(crate) inputs: BTreeMap, pub(crate) expected: BTreeMap, pub(crate) rtol: f64, pub(crate) atol: f64, } +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(untagged)] +pub(crate) enum GoldenInput { + Number(f64), + Enum(String), +} + #[derive(Clone, Debug, Deserialize, Serialize)] pub(crate) struct PublicApi { pub(crate) name: String, @@ -236,6 +430,105 @@ pub(crate) struct Parameter { pub(crate) description: String, } +#[derive(Clone, Debug, Serialize)] +#[serde(untagged)] +pub(crate) enum Input { + Parameter(Parameter), + Enum { + name: String, + description: Option, + #[serde(skip)] + definition: EnumDefinition, + }, +} + +impl Input { + pub(crate) fn name(&self) -> &str { + match self { + Self::Parameter(parameter) => ¶meter.name, + Self::Enum { name, .. } => name, + } + } + + pub(crate) fn description(&self) -> &str { + match self { + Self::Parameter(parameter) => ¶meter.description, + Self::Enum { description, .. } => description.as_deref().unwrap_or_default(), + } + } + + pub(crate) fn unit(&self) -> Option<&str> { + match self { + Self::Parameter(parameter) => Some(¶meter.unit), + Self::Enum { .. } => None, + } + } + + pub(crate) fn domain(&self) -> Option<&str> { + match self { + Self::Parameter(parameter) => parameter.domain.as_deref(), + Self::Enum { .. } => None, + } + } + + pub(crate) fn enum_type(&self) -> Option<&EnumDefinition> { + match self { + Self::Parameter(_) => None, + Self::Enum { definition, .. } => Some(definition), + } + } +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub(crate) struct EnumDefinition { + #[serde(skip)] + pub(crate) name: String, + #[serde(rename = "type")] + kind: EnumKind, + pub(crate) description: String, + pub(crate) values: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "lowercase")] +enum EnumKind { + Enum, +} + +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub(crate) struct EnumValue { + pub(crate) name: String, + pub(crate) value: String, + pub(crate) description: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct LookupDefinition { + #[serde(skip)] + pub(crate) name: String, + #[serde(rename = "type")] + kind: LookupKind, + pub(crate) input: Reference, + pub(crate) output: Reference, + pub(crate) values: Vec, + #[serde(skip)] + pub(crate) input_type: Option, + #[serde(skip)] + pub(crate) output_type: Option, +} + +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +enum LookupKind { + Lookup, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct LookupValue { + pub(crate) key: String, + pub(crate) value: BTreeMap, +} + #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "type", rename_all = "lowercase")] pub(crate) enum Outputs { @@ -282,14 +575,28 @@ pub(crate) struct Generation { #[derive(Clone, Debug, Deserialize, Serialize)] pub(crate) struct Implementation { - #[serde(default)] pub(crate) variables: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] -pub(crate) struct ImplementationVariable { - pub(crate) name: String, - pub(crate) expr: String, +#[serde(untagged)] +pub(crate) enum ImplementationVariable { + Expression { + name: String, + expr: String, + }, + Lookup { + name: String, + lookup: Box, + }, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub(crate) struct LookupInvocation { + pub(crate) table: Reference, + pub(crate) key: String, + #[serde(skip)] + pub(crate) definition: Option, } #[derive(Clone, Debug)] @@ -313,12 +620,21 @@ pub(crate) struct CompiledFunction { #[derive(Clone, Debug)] pub(crate) struct CompiledGoldenTest { pub(crate) id: String, - pub(crate) inputs: Vec, + pub(crate) inputs: Vec, pub(crate) expected: Vec, pub(crate) rtol: f64, pub(crate) atol: f64, } +#[derive(Clone, Debug)] +pub(crate) enum CompiledInput { + Number(f64), + Enum { + enum_name: String, + member_name: String, + }, +} + #[derive(Clone, Debug)] pub(crate) struct CoreFunction { pub(crate) name: String, @@ -344,12 +660,32 @@ pub(crate) struct RawFunction { #[derive(Clone, Debug)] pub(crate) struct RawInput { pub(crate) name: String, + pub(crate) value_type: RawInputType, +} + +#[derive(Clone, Debug)] +pub(crate) enum RawInputType { + Number, + Enum(EnumDefinition), } #[derive(Clone, Debug)] pub(crate) struct RawVariable { pub(crate) name: String, - pub(crate) expression: RawExpression, + pub(crate) value: RawVariableValue, +} + +#[derive(Clone, Debug)] +pub(crate) enum RawVariableValue { + Expression(RawExpression), + Lookup(RawLookup), +} + +#[derive(Clone, Debug)] +pub(crate) struct RawLookup { + pub(crate) implementation_path: String, + pub(crate) key: String, + pub(crate) definition: LookupDefinition, } #[derive(Clone, Debug)] @@ -389,7 +725,7 @@ mod tests { } #[test] - fn resolves_reusable_input_and_output_names() { + fn resolves_named_enum_input_and_reusable_output() { let spec: Spec = serde_yaml::from_str( r##" source: @@ -397,12 +733,12 @@ source: citation_apa: Test (2026). doi: null $defs: - x: - name: x - symbol: x - unit: '1' - domain: null - description: Test input. + TestCategory: + type: enum + description: Test category type. + values: + - name: first + value: first reusable_result: type: record name: TestResult @@ -422,7 +758,9 @@ functions: prediction_target: Test result. models: {h_theta: null, k_h: null} inputs: - - $ref: "#/$defs/x" + - $ref: "#/$defs/TestCategory" + name: x + description: Test input category. outputs: $ref: "#/$defs/reusable_result" "##, @@ -430,8 +768,39 @@ functions: .expect("reusable schemas deserialize"); let function = &spec.functions[0]; - assert_eq!(function.inputs[0].name, "x"); + assert_eq!(function.inputs[0].name(), "x"); + assert_eq!(function.inputs[0].description(), "Test input category."); + assert_eq!( + function.inputs[0] + .enum_type() + .expect("enum input is resolved") + .name, + "TestCategory" + ); assert_eq!(function.outputs.fields()[0].name, "value"); assert_eq!(function.result_class(), Some("TestResult")); } + + #[test] + fn does_not_use_an_enum_type_description_for_an_undocumented_binding() { + let input = Input::Enum { + name: "topsoil_texture".into(), + description: None, + definition: EnumDefinition { + name: "TestCategory".into(), + kind: EnumKind::Enum, + description: "Test category type.".into(), + values: Vec::new(), + }, + }; + + assert_eq!(input.description(), ""); + assert_eq!( + input + .enum_type() + .expect("enum input retains its type") + .description, + "Test category type." + ); + } } diff --git a/codegen/src/render/c.rs b/codegen/src/render/c.rs index ad29a9d..b45be8b 100644 --- a/codegen/src/render/c.rs +++ b/codegen/src/render/c.rs @@ -36,7 +36,7 @@ pub(crate) fn test_float_literal(value: f64) -> String { pub(crate) fn requires_math(expression: &Expr) -> bool { match expression { - Expr::Number(_) | Expr::Reference(_) => false, + Expr::Number(_) | Expr::Reference(_) | Expr::Field { .. } => false, Expr::Unary { operand, .. } => requires_math(operand), Expr::Binary { op, left, right } => { (matches!(op, BinaryOp::Power) && small_integer_exponent(right).is_none()) @@ -49,7 +49,7 @@ pub(crate) fn requires_math(expression: &Expr) -> bool { pub(crate) fn requires_pow4(expression: &Expr) -> bool { match expression { - Expr::Number(_) | Expr::Reference(_) => false, + Expr::Number(_) | Expr::Reference(_) | Expr::Field { .. } => false, Expr::Unary { operand, .. } => requires_pow4(operand), Expr::Binary { op, left, right } => { (matches!(op, BinaryOp::Power) && small_integer_exponent(right) == Some(4)) @@ -112,6 +112,13 @@ impl Expression<'_> { Expr::Reference(Reference::Variable(index)) => { write!(formatter, "{}", self.variables[*index].name)?; } + Expr::Field { record, field } => { + let name = match record { + Reference::Input(index) => &self.inputs[*index], + Reference::Variable(index) => &self.variables[*index].name, + }; + write!(formatter, "{name}.{field}")? + } Expr::Unary { op, operand } => match op { UnaryOp::Plus => unreachable!("unary plus is handled before parenthesizing"), UnaryOp::Minus => { @@ -245,9 +252,11 @@ fn precedence(expression: &Expr) -> Precedence { right, .. } if small_integer_exponent(right).is_some() => Precedence::Product, - Expr::Number(_) | Expr::Reference(_) | Expr::Binary { .. } | Expr::Call { .. } => { - Precedence::Primary - } + Expr::Number(_) + | Expr::Reference(_) + | Expr::Field { .. } + | Expr::Binary { .. } + | Expr::Call { .. } => Precedence::Primary, } } @@ -430,4 +439,26 @@ mod tests { "x / (y * y)" ); } + + #[test] + fn renders_record_field_access_for_native_dialects() { + let expression = Expr::Field { + record: Reference::Variable(0), + field: "b".into(), + }; + let variables = [Variable { + name: "parameters".into(), + value: crate::semantic::VariableValue::Number(Expr::Number(Number { + value: 0.0, + lexeme: "0.0".into(), + })), + }]; + + assert_eq!( + super::expression(&expression, &[], &variables, Dialect::C).to_string(), + "parameters.b" + ); + assert!(!requires_math(&expression)); + assert!(!requires_pow4(&expression)); + } } diff --git a/codegen/src/semantic.rs b/codegen/src/semantic.rs index b322ce7..c40e61a 100644 --- a/codegen/src/semantic.rs +++ b/codegen/src/semantic.rs @@ -5,24 +5,76 @@ use std::{ use crate::{ formula::{self, Span}, - model::{RawExpression, RawFunction, SourceLocation}, + model::{ + RawExpression, RawFunction, RawInputType, RawLookup, RawVariableValue, SourceLocation, + }, }; #[derive(Clone, Debug, PartialEq)] pub(crate) struct Function { pub(crate) inputs: Vec, pub(crate) variables: Vec, + pub(crate) result: ResultBinding, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum ResultBinding { + Fields, + RecordVariable(usize), } #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct Input { pub(crate) name: String, + pub(crate) value_type: ValueType, } #[derive(Clone, Debug, PartialEq)] pub(crate) struct Variable { pub(crate) name: String, - pub(crate) expression: Expr, + pub(crate) value: VariableValue, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) enum ValueType { + Number, + Enum(String), + Record(RecordType), +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct RecordType { + pub(crate) name: String, + pub(crate) fields: Vec, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) enum VariableValue { + Number(Expr), + RecordLookup(RecordLookup), +} + +impl VariableValue { + pub(crate) fn as_number(&self) -> Option<&Expr> { + match self { + Self::Number(expression) => Some(expression), + Self::RecordLookup(_) => None, + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct RecordLookup { + pub(crate) key: Reference, + pub(crate) enum_name: String, + pub(crate) output: RecordType, + pub(crate) cases: Vec, +} + +#[derive(Clone, Debug, PartialEq)] +pub(crate) struct RecordLookupCase { + pub(crate) member: String, + pub(crate) values: Vec, } #[derive(Clone, Debug, PartialEq)] @@ -35,6 +87,10 @@ pub(crate) struct Number { pub(crate) enum Expr { Number(Number), Reference(Reference), + Field { + record: Reference, + field: String, + }, Unary { op: UnaryOp, operand: Box, @@ -144,16 +200,29 @@ impl fmt::Display for Error { impl std::error::Error for Error {} pub(crate) fn compile(raw: &RawFunction) -> Result { - let mut inputs = BTreeMap::new(); + let mut scope = BTreeMap::new(); for (index, input) in raw.inputs.iter().enumerate() { - insert_name( - raw, - &mut inputs, - &input.name, - "inputs", - Span { start: 0, end: 0 }, - )?; - debug_assert_eq!(inputs[&input.name], index); + let value_type = match &input.value_type { + RawInputType::Number => ValueType::Number, + RawInputType::Enum(definition) => ValueType::Enum(definition.name.clone()), + }; + if scope + .insert( + input.name.clone(), + Binding { + reference: Reference::Input(index), + value_type, + }, + ) + .is_some() + { + return Err(error( + raw, + "inputs", + Span { start: 0, end: 0 }, + format!("duplicate name `{}`", input.name), + )); + } } validate_repeated_expressions(raw)?; @@ -165,7 +234,6 @@ pub(crate) fn compile(raw: &RawFunction) -> Result { .map(|(index, variable)| (variable.name.as_str(), index)) .collect(); let mut variables = Vec::with_capacity(raw.variables.len()); - let mut scope = inputs; for (index, variable) in raw.variables.iter().enumerate() { if scope.contains_key(&variable.name) { return Err(error( @@ -175,12 +243,35 @@ pub(crate) fn compile(raw: &RawFunction) -> Result { format!("duplicate name `{}`", variable.name), )); } - let expression = - compile_expression(raw, &variable.expression, &scope, &variable_names, index)?; - scope.insert(variable.name.clone(), raw.inputs.len() + index); + let (value, value_type) = match &variable.value { + RawVariableValue::Expression(expression) => ( + VariableValue::Number(compile_expression( + raw, + expression, + &scope, + &variable_names, + index, + )?), + ValueType::Number, + ), + RawVariableValue::Lookup(lookup) => { + let (lookup, record_type) = compile_lookup(raw, lookup, &scope)?; + ( + VariableValue::RecordLookup(lookup), + ValueType::Record(record_type), + ) + } + }; + scope.insert( + variable.name.clone(), + Binding { + reference: Reference::Variable(index), + value_type, + }, + ); variables.push(Variable { name: variable.name.clone(), - expression, + value, }); } @@ -190,9 +281,14 @@ pub(crate) fn compile(raw: &RawFunction) -> Result { .iter() .map(|input| Input { name: input.name.clone(), + value_type: match &input.value_type { + RawInputType::Number => ValueType::Number, + RawInputType::Enum(definition) => ValueType::Enum(definition.name.clone()), + }, }) .collect(), variables, + result: ResultBinding::Fields, }) } @@ -200,6 +296,10 @@ pub(crate) fn compile(raw: &RawFunction) -> Result { enum StructuralExpr { Number(String), Variable(String), + Field { + base: String, + field: String, + }, Unary { op: formula::UnaryOp, operand: Box, @@ -225,7 +325,9 @@ struct Occurrence { fn validate_repeated_expressions(raw: &RawFunction) -> Result<(), Error> { let mut occurrences = HashMap::new(); for variable in &raw.variables { - let expression = &variable.expression; + let RawVariableValue::Expression(expression) = &variable.value else { + continue; + }; if let Some((first, later)) = find_repeated_expression( &expression.expression, expression.source_location, @@ -264,7 +366,6 @@ fn find_repeated_expression( } occurrences.insert(structural, occurrence); } - match &expression.kind { formula::ExprKind::Unary { operand, .. } | formula::ExprKind::Grouped(operand) => { find_repeated_expression(operand, source_location, occurrences) @@ -276,7 +377,9 @@ fn find_repeated_expression( formula::ExprKind::Call { args, .. } => args .iter() .find_map(|argument| find_repeated_expression(argument, source_location, occurrences)), - formula::ExprKind::Number(_) | formula::ExprKind::Variable(_) => None, + formula::ExprKind::Number(_) + | formula::ExprKind::Variable(_) + | formula::ExprKind::Field { .. } => None, } } @@ -291,13 +394,17 @@ fn requires_extraction(expression: &formula::Expr) -> bool { formula::ExprKind::Unary { operand, .. } | formula::ExprKind::Grouped(operand) => { arithmetic_operation_count(expression) > 1 || requires_extraction(operand) } - formula::ExprKind::Number(_) | formula::ExprKind::Variable(_) => false, + formula::ExprKind::Number(_) + | formula::ExprKind::Variable(_) + | formula::ExprKind::Field { .. } => false, } } fn arithmetic_operation_count(expression: &formula::Expr) -> usize { match &expression.kind { - formula::ExprKind::Number(_) | formula::ExprKind::Variable(_) => 0, + formula::ExprKind::Number(_) + | formula::ExprKind::Variable(_) + | formula::ExprKind::Field { .. } => 0, formula::ExprKind::Call { args, .. } => args.iter().map(arithmetic_operation_count).sum(), formula::ExprKind::Grouped(operand) => arithmetic_operation_count(operand), formula::ExprKind::Unary { operand, .. } => 1 + arithmetic_operation_count(operand), @@ -311,6 +418,10 @@ fn structural_expr(expression: &formula::Expr) -> StructuralExpr { match &expression.kind { formula::ExprKind::Number(number) => StructuralExpr::Number(number.lexeme.clone()), formula::ExprKind::Variable(name) => StructuralExpr::Variable(name.clone()), + formula::ExprKind::Field { base, field } => StructuralExpr::Field { + base: base.clone(), + field: field.clone(), + }, formula::ExprKind::Unary { op, operand } => StructuralExpr::Unary { op: *op, operand: Box::new(structural_expr(operand)), @@ -330,23 +441,86 @@ fn structural_expr(expression: &formula::Expr) -> StructuralExpr { } } -fn insert_name( +#[derive(Clone)] +struct Binding { + reference: Reference, + value_type: ValueType, +} + +fn compile_lookup( raw: &RawFunction, - names: &mut BTreeMap, - name: &str, - path: &str, - span: Span, -) -> Result<(), Error> { - if names.insert(name.to_owned(), names.len()).is_some() { - return Err(error(raw, path, span, format!("duplicate name `{name}`"))); + lookup: &RawLookup, + scope: &BTreeMap, +) -> Result<(RecordLookup, RecordType), Error> { + let binding = scope.get(&lookup.key).ok_or_else(|| { + error( + raw, + &lookup.implementation_path, + Span { start: 0, end: 0 }, + format!("unknown lookup key `{}`", lookup.key), + ) + })?; + let enum_type = lookup + .definition + .input_type + .as_ref() + .expect("lookup input type is resolved"); + if binding.value_type != ValueType::Enum(enum_type.name.clone()) { + return Err(error( + raw, + &lookup.implementation_path, + Span { start: 0, end: 0 }, + format!( + "lookup key `{}` must have enum type `{}`", + lookup.key, enum_type.name + ), + )); } - Ok(()) + let output = lookup + .definition + .output_type + .as_ref() + .expect("lookup output type is resolved"); + let crate::model::Outputs::Record { name, fields } = output else { + unreachable!("lookup outputs are validated as records") + }; + let record_type = RecordType { + name: name.clone(), + fields: fields.iter().map(|field| field.name.clone()).collect(), + }; + let cases = lookup + .definition + .values + .iter() + .map(|case| RecordLookupCase { + member: case.key.clone(), + values: fields + .iter() + .map(|field| { + let value = case.value[&field.name]; + Number { + value, + lexeme: format!("{value:?}"), + } + }) + .collect(), + }) + .collect(); + Ok(( + RecordLookup { + key: binding.reference, + enum_name: enum_type.name.clone(), + output: record_type.clone(), + cases, + }, + record_type, + )) } fn compile_expression( raw: &RawFunction, expression: &RawExpression, - scope: &BTreeMap, + scope: &BTreeMap, variable_names: &BTreeMap<&str, usize>, variable_index: usize, ) -> Result { @@ -364,7 +538,7 @@ fn compile_expr( raw: &RawFunction, source: &RawExpression, expression: &formula::Expr, - scope: &BTreeMap, + scope: &BTreeMap, variable_names: &BTreeMap<&str, usize>, variable_index: usize, ) -> Result { @@ -374,12 +548,15 @@ fn compile_expr( lexeme: number.lexeme.clone(), })), formula::ExprKind::Variable(name) => match scope.get(name) { - Some(index) if *index < raw.inputs.len() => { - Ok(Expr::Reference(Reference::Input(*index))) + Some(binding) if binding.value_type == ValueType::Number => { + Ok(Expr::Reference(binding.reference)) } - Some(index) => Ok(Expr::Reference(Reference::Variable( - *index - raw.inputs.len(), - ))), + Some(_) => Err(expression_error( + raw, + source, + expression.span, + format!("identifier `{name}` is not numeric"), + )), None => { let message = match variable_names.get(name.as_str()) { Some(index) if *index == variable_index => { @@ -393,6 +570,36 @@ fn compile_expr( Err(expression_error(raw, source, expression.span, message)) } }, + formula::ExprKind::Field { base, field } => match scope.get(base) { + Some(Binding { + reference, + value_type: ValueType::Record(record), + }) if record.fields.contains(field) => Ok(Expr::Field { + record: *reference, + field: field.clone(), + }), + Some(Binding { + value_type: ValueType::Record(record), + .. + }) => Err(expression_error( + raw, + source, + expression.span, + format!("record `{}` has no field `{field}`", record.name), + )), + Some(_) => Err(expression_error( + raw, + source, + expression.span, + format!("`{base}` is not a record"), + )), + None => Err(expression_error( + raw, + source, + expression.span, + format!("unknown identifier `{base}`"), + )), + }, formula::ExprKind::Unary { op, operand } => Ok(Expr::Unary { op: match op { formula::UnaryOp::Plus => UnaryOp::Plus, @@ -521,7 +728,10 @@ mod tests { use crate::{ formula::parse, - model::{RawExpression, RawFunction, RawInput, RawVariable, SourceLocation}, + model::{ + RawExpression, RawFunction, RawInput, RawInputType, RawVariable, RawVariableValue, + SourceLocation, + }, }; use super::{BinaryOp, Expr, compile}; @@ -542,24 +752,29 @@ mod tests { .iter() .map(|name| RawInput { name: (*name).into(), + value_type: RawInputType::Number, }) .collect(), variables, } } + fn variable(name: &str, path: &str, source: &str) -> RawVariable { + RawVariable { + name: name.into(), + value: RawVariableValue::Expression(expression(path, source)), + } + } + #[test] fn compiles_variables() { let raw = function( &["x"], - vec![RawVariable { - name: "twice".into(), - expression: expression("implementation.variables[0]", "x * 2"), - }], + vec![variable("twice", "implementation.variables[0]", "x * 2")], ); let compiled = compile(&raw).unwrap(); assert!(matches!( - compiled.variables[0].expression, + compiled.variables[0].value.as_number().unwrap(), Expr::Binary { op: BinaryOp::Multiply, .. @@ -572,164 +787,17 @@ mod tests { let raw = function( &["x"], vec![ - RawVariable { - name: "first".into(), - expression: expression("implementation.variables[0]", "x + 1"), - }, - RawVariable { - name: "second".into(), - expression: expression("implementation.variables[1]", "first * first"), - }, + variable("first", "implementation.variables[0]", "x + 1"), + variable("second", "implementation.variables[1]", "first * first"), ], ); let compiled = compile(&raw).unwrap(); assert!(matches!( - compiled.variables[1].expression, + compiled.variables[1].value.as_number().unwrap(), Expr::Binary { .. } )); } - #[test] - fn permits_repeated_simple_multiplication() { - let raw = function( - &["x", "y"], - vec![ - RawVariable { - name: "first".into(), - expression: expression("implementation.variables[0].expr", "x * y"), - }, - RawVariable { - name: "second".into(), - expression: expression("implementation.variables[1].expr", "x * y"), - }, - ], - ); - - assert!(compile(&raw).is_ok()); - } - - #[test] - fn rejects_repeated_power_with_both_expression_locations() { - let mut raw = function( - &["x"], - vec![ - RawVariable { - name: "first".into(), - expression: expression("implementation.variables[0].expr", "x ^ 2"), - }, - RawVariable { - name: "second".into(), - expression: expression("implementation.variables[1].expr", "x ^ 2"), - }, - ], - ); - raw.variables[1].expression.source_location = SourceLocation { line: 2, column: 3 }; - - let error = compile(&raw).unwrap_err().to_string(); - assert!( - error.contains("specs/functions/example.md:1:1..6"), - "{error}" - ); - assert!( - error.contains("specs/functions/example.md:2:3..8"), - "{error}" - ); - assert!( - error.contains("must be extracted into an earlier implementation variable"), - "{error}" - ); - } - - #[test] - fn rejects_repeated_math_function_calls() { - let raw = function( - &["x"], - vec![ - RawVariable { - name: "first".into(), - expression: expression("implementation.variables[0].expr", "sqrt(x)"), - }, - RawVariable { - name: "second".into(), - expression: expression("implementation.variables[1].expr", "sqrt(x)"), - }, - ], - ); - - assert!( - compile(&raw) - .unwrap_err() - .to_string() - .contains("must be extracted into an earlier implementation variable") - ); - } - - #[test] - fn rejects_repeated_compound_arithmetic_expressions() { - let raw = function( - &["x", "y", "z"], - vec![ - RawVariable { - name: "first".into(), - expression: expression("implementation.variables[0].expr", "x + y * z"), - }, - RawVariable { - name: "second".into(), - expression: expression("implementation.variables[1].expr", "x + y * z"), - }, - ], - ); - - assert!( - compile(&raw) - .unwrap_err() - .to_string() - .contains("must be extracted into an earlier implementation variable") - ); - } - - #[test] - fn distinguishes_operand_order() { - let raw = function( - &["x", "y"], - vec![ - RawVariable { - name: "first".into(), - expression: expression("implementation.variables[0].expr", "x / y"), - }, - RawVariable { - name: "second".into(), - expression: expression("implementation.variables[1].expr", "y / x"), - }, - ], - ); - - assert!(compile(&raw).is_ok()); - } - - #[test] - fn permits_references_to_an_earlier_extracted_expression() { - let raw = function( - &["x"], - vec![ - RawVariable { - name: "x_squared".into(), - expression: expression("implementation.variables[0].expr", "x ^ 2"), - }, - RawVariable { - name: "first".into(), - expression: expression("implementation.variables[1].expr", "x_squared + 1"), - }, - RawVariable { - name: "second".into(), - expression: expression("implementation.variables[2].expr", "x_squared + 2"), - }, - ], - ); - - assert!(compile(&raw).is_ok()); - } - #[test] fn rejects_unknown_forward_and_self_references_with_expression_paths() { for (source, expected) in [ @@ -738,14 +806,8 @@ mod tests { ("current", "cannot reference itself"), ] { let variables = vec![ - RawVariable { - name: "current".into(), - expression: expression("implementation.variables[0]", source), - }, - RawVariable { - name: "later".into(), - expression: expression("implementation.variables[1]", "1"), - }, + variable("current", "implementation.variables[0]", source), + variable("later", "implementation.variables[1]", "1"), ]; let error = compile(&function(&[], variables)).unwrap_err(); assert!(error.to_string().contains(expected), "{error}"); @@ -767,14 +829,8 @@ mod tests { let duplicate_variable = function( &[], vec![ - RawVariable { - name: "x".into(), - expression: expression("implementation.variables[0]", "1"), - }, - RawVariable { - name: "x".into(), - expression: expression("implementation.variables[1]", "1"), - }, + variable("x", "implementation.variables[0]", "1"), + variable("x", "implementation.variables[1]", "1"), ], ); assert!( @@ -789,10 +845,7 @@ mod tests { fn rejects_unknown_functions_and_wrong_arities() { let unknown = function( &["x"], - vec![RawVariable { - name: "value".into(), - expression: expression("implementation.variables[0]", "nope(x)"), - }], + vec![variable("value", "implementation.variables[0]", "nope(x)")], ); assert!( compile(&unknown) @@ -811,10 +864,7 @@ mod tests { ] { let raw = function( &["x"], - vec![RawVariable { - name: "value".into(), - expression: expression("implementation.variables[0]", source), - }], + vec![variable("value", "implementation.variables[0]", source)], ); assert!( compile(&raw).unwrap_err().to_string().contains("expects"), diff --git a/codegen/src/specs.rs b/codegen/src/specs.rs index cddf040..065b8ae 100644 --- a/codegen/src/specs.rs +++ b/codegen/src/specs.rs @@ -6,7 +6,10 @@ use serde_json::Value; use crate::{ formula, - model::{Entry, Implementation, RawExpression, RawFunction, RawInput, RawVariable, Spec}, + model::{ + Entry, Implementation, ImplementationVariable, Input, RawExpression, RawFunction, RawInput, + RawInputType, RawLookup, RawVariable, RawVariableValue, Spec, + }, semantic, }; @@ -157,37 +160,60 @@ fn compile( .inputs .iter() .map(|input| RawInput { - name: input.name.clone(), + name: input.name().to_owned(), + value_type: match input { + Input::Parameter(_) => RawInputType::Number, + Input::Enum { definition, .. } => RawInputType::Enum(definition.clone()), + }, }) .collect(), variables: implementation .variables .iter() .enumerate() - .map(|(index, variable)| { - let source_location = expression_locations.next().ok_or_else(|| { - format!( - "{} -> function {} -> implementation.variables[{index}].expr: source location is unavailable", - path.display(), - function.name + .map(|(index, variable)| match variable { + ImplementationVariable::Expression { name, expr } => { + let source_location = expression_locations.next().ok_or_else(|| { + format!( + "{} -> function {} -> implementation.variables[{index}].expr: source location is unavailable", + path.display(), + function.name + ) + })?; + expression( + path, + &function.name, + format!("implementation.variables[{index}].expr"), + expr, + source_location, ) - })?; - expression( - path, - &function.name, - format!("implementation.variables[{index}].expr"), - &variable.expr, - source_location, - ) + } .map(|expression| RawVariable { - name: variable.name.clone(), - expression, - }) + name: name.clone(), + value: RawVariableValue::Expression(expression), + }), + ImplementationVariable::Lookup { name, lookup } => { + let definition = lookup + .definition + .clone() + .expect("lookup invocation is resolved"); + Ok(RawVariable { + name: name.clone(), + value: RawVariableValue::Lookup(RawLookup { + implementation_path: format!( + "implementation.variables[{index}].lookup" + ), + key: lookup.key.clone(), + definition, + }), + }) + } }) .collect::, _>>()?, }; - validate_output(path, function, &raw)?; - semantic::compile(&raw).map_err(|error| error.to_string()) + let mut compiled = semantic::compile(&raw).map_err(|error| error.to_string())?; + compiled.result = validate_output(path, function, &compiled)?; + Ok(compiled) } fn expression( @@ -219,7 +245,10 @@ fn expression_locations( .iter() .filter_map(|function| function.implementation.as_ref()) .flat_map(|implementation| implementation.variables.iter()) - .map(|variable| variable.expr.as_str()); + .filter_map(|variable| match variable { + ImplementationVariable::Expression { expr, .. } => Some(expr.as_str()), + ImplementationVariable::Lookup { .. } => None, + }); let mut cursor = 0; let mut locations = Vec::new(); @@ -267,26 +296,60 @@ fn location(text: &str, offset: usize) -> crate::model::SourceLocation { fn validate_output( path: &Path, function: &crate::model::Function, - raw: &RawFunction, -) -> Result<(), String> { + compiled: &semantic::Function, +) -> Result { + if let crate::model::Outputs::Record { name, fields } = &function.outputs { + let expected = semantic::RecordType { + name: name.clone(), + fields: fields.iter().map(|field| field.name.clone()).collect(), + }; + if let Some(( + index, + semantic::Variable { + value: semantic::VariableValue::RecordLookup(lookup), + .. + }, + )) = compiled.variables.iter().enumerate().next_back() + { + if lookup.output == expected { + return Ok(semantic::ResultBinding::RecordVariable(index)); + } + if lookup.output.name == expected.name { + return Err(format!( + "{} -> function {} -> implementation.variables[{index}]: record lookup type `{}` does not exactly match the function output record", + path.display(), + function.name, + lookup.output.name + )); + } + } + } let output_names = function .outputs .fields() .iter() .map(|output| &output.name) .collect::>(); - let output_sources = raw + let output_sources = compiled .inputs .iter() .map(|input| input.name.as_str()) - .chain(raw.variables.iter().map(|variable| variable.name.as_str())) + .chain( + compiled + .variables + .iter() + .filter_map(|variable| match variable.value { + semantic::VariableValue::Number(_) => Some(variable.name.as_str()), + semantic::VariableValue::RecordLookup(_) => None, + }), + ) .collect::>(); let missing = output_names .iter() .filter(|name| !output_sources.contains(name.as_str())) .collect::>(); if missing.is_empty() { - Ok(()) + Ok(semantic::ResultBinding::Fields) } else { Err(format!( "{} -> function {} -> implementation.variables: missing final output variables {:?}", @@ -313,7 +376,7 @@ mod tests { path::{Path, PathBuf}, }; - use super::{find_expression, load, location}; + use super::load; use crate::model::PythonGeneration; fn fixture_root(label: &str) -> PathBuf { @@ -363,6 +426,190 @@ mod tests { fs::remove_dir_all(root).unwrap(); } + #[test] + fn compiles_a_record_lookup_with_additional_inputs_and_later_field_expressions() { + let root = fixture_root("record-lookup-expression"); + let specification = r#"source: + summary: Test source. + citation_apa: Test (2026). + doi: null +$defs: + Texture: + type: enum + description: Texture class. + values: + - {name: sand, value: sand} + - {name: clay, value: clay} + Parameters: + type: record + name: Parameters + fields: + - {name: factor, symbol: f, unit: '1', domain: null, description: Test factor.} + ParametersByTexture: + type: lookup + input: {$ref: '#/$defs/Texture'} + output: {$ref: '#/$defs/Parameters'} + values: + - {key: sand, value: {factor: 2.0}} + - {key: clay, value: {factor: 3.0}} +functions: + - name: calc_ptf_record_lookup_expression + status: ready-for-implementation + public_api: {name: calc_ptf_record_lookup_expression, result_class: null, summary: Test value.} + scope: + prediction_target: Test value. + models: {h_theta: null, k_h: null} + inputs: + - {$ref: '#/$defs/Texture', name: texture} + - {name: x, symbol: x, unit: '1', domain: null, description: Test input.} + outputs: {type: scalar, name: value, symbol: y, unit: '1', domain: null, description: Test output.} + implementation: + variables: + - name: parameters + lookup: + table: {$ref: '#/$defs/ParametersByTexture'} + key: texture + - {name: value, expr: parameters.factor * x} +"#; + fs::write( + root.join("specs/functions/record_lookup_expression.yaml"), + specification, + ) + .unwrap(); + + let entries = load(&root).unwrap(); + let implementation = entries[0].implementations[0].as_ref().unwrap(); + assert!(matches!( + implementation.variables[0].value, + crate::semantic::VariableValue::RecordLookup(_) + )); + assert!(matches!( + implementation.variables[1].value, + crate::semantic::VariableValue::Number(_) + )); + let compiled = crate::compile::functions(entries).unwrap(); + let rust = crate::targets::render_rust_for_test(&compiled).unwrap(); + let rust = &rust + .iter() + .find(|file| file.path.ends_with("record_lookup_expression.rs")) + .unwrap() + .contents; + assert!(rust.contains("struct Parameters"), "{rust}"); + assert!(rust.contains("parameters . factor"), "{rust}"); + let (c_headers, cpp_modules) = crate::targets::render_native_for_test(&compiled).unwrap(); + let c = &c_headers + .iter() + .find(|file| file.path.ends_with("record_lookup_expression.h")) + .unwrap() + .contents; + assert!(c.contains("} parameters;"), "{c}"); + assert!(c.contains("parameters.factor"), "{c}"); + let cpp = &cpp_modules + .iter() + .find(|file| file.path.ends_with("record_lookup_expression.cppm")) + .unwrap() + .contents; + assert!(cpp.contains("struct Parameters"), "{cpp}"); + assert!(cpp.contains("parameters.factor"), "{cpp}"); + let extension = crate::targets::render_python_extension_for_test(&compiled).unwrap(); + let extension = &extension + .iter() + .find(|file| file.path.ends_with("record_lookup_expression.c")) + .unwrap() + .contents; + assert!( + extension.contains("const npy_uint32 texture = in_texture[index];"), + "{extension}" + ); + assert!( + extension.contains("calc_ptf_record_lookup_expression(texture, x)"), + "{extension}" + ); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn validates_unused_lookup_definitions() { + let root = fixture_root("unused-invalid-lookup"); + let specification = specification( + "unused_invalid_lookup", + " implementation:\n variables: [{name: value, expr: x}]\n", + "", + ) + .replace( + "functions:\n", + "$defs:\n Texture:\n type: enum\n description: Texture class.\n values:\n - {name: sand, value: sand}\n - {name: clay, value: clay}\n Parameters:\n type: record\n name: Parameters\n fields:\n - {name: factor, symbol: f, unit: '1', domain: null, description: Test factor.}\n InvalidLookup:\n type: lookup\n input: {$ref: '#/$defs/Texture'}\n output: {$ref: '#/$defs/Parameters'}\n values:\n - {key: sand, value: {factor: 2.0}}\nfunctions:\n", + ); + fs::write( + root.join("specs/functions/unused_invalid_lookup.yaml"), + specification, + ) + .unwrap(); + + let error = load(&root).unwrap_err().to_string(); + assert!(error.contains("lookup `InvalidLookup` values must cover every member")); + fs::remove_dir_all(root).unwrap(); + } + + #[test] + fn rejects_a_record_lookup_that_only_matches_the_output_name() { + let root = fixture_root("mismatched-record-lookup"); + let specification = r#"source: + summary: Test source. + citation_apa: Test (2026). + doi: null +$defs: + Texture: + type: enum + description: Texture class. + values: + - {name: sand, value: sand} + LookupParameters: + type: record + name: Parameters + fields: + - {name: factor, symbol: f, unit: '1', domain: null, description: Test factor.} + ParametersByTexture: + type: lookup + input: {$ref: '#/$defs/Texture'} + output: {$ref: '#/$defs/LookupParameters'} + values: + - {key: sand, value: {factor: 2.0}} +functions: + - name: calc_ptf_mismatched_record_lookup + status: ready-for-implementation + public_api: {name: calc_ptf_mismatched_record_lookup, result_class: Parameters, summary: Test value.} + scope: + prediction_target: Test value. + models: {h_theta: null, k_h: null} + inputs: + - {$ref: '#/$defs/Texture', name: texture} + outputs: + type: record + name: Parameters + fields: + - {name: other, symbol: o, unit: '1', domain: null, description: Other value.} + implementation: + variables: + - name: parameters + lookup: + table: {$ref: '#/$defs/ParametersByTexture'} + key: texture +"#; + fs::write( + root.join("specs/functions/mismatched_record_lookup.yaml"), + specification, + ) + .unwrap(); + + let error = load(&root).unwrap_err().to_string(); + assert!( + error.contains("record lookup type `Parameters` does not exactly match"), + "{error}" + ); + fs::remove_dir_all(root).unwrap(); + } + #[test] fn permits_an_output_to_reuse_a_same_named_input() { let root = fixture_root("input-output"); @@ -530,16 +777,4 @@ mod tests { assert!(error.contains("filename stem must be an APA-style slug")); } - - #[test] - fn locates_block_and_quoted_formula_values_in_yaml_source() { - let source = "variables:\n - expr: >-\n x ^ 2\n - {expr: 'sqrt(x)'}\n"; - let (power, cursor) = find_expression(source, 0, "x ^ 2").unwrap(); - let (call, _) = find_expression(source, cursor, "sqrt(x)").unwrap(); - - assert_eq!(location(source, power).line, 3); - assert_eq!(location(source, power).column, 7); - assert_eq!(location(source, call).line, 4); - assert_eq!(location(source, call).column, 13); - } } diff --git a/codegen/src/targets/catalog.rs b/codegen/src/targets/catalog.rs index 5bf3284..b142d4b 100644 --- a/codegen/src/targets/catalog.rs +++ b/codegen/src/targets/catalog.rs @@ -116,19 +116,15 @@ impl Render for FunctionSection<'_> { writer.blank_line(); } - ParameterTable { - title: "Inputs", - parameters: self.document.parameters, - } - .render(writer); - ParameterTable { - title: "Outputs", - parameters: match self.document.returns { + render_input_table(writer, self.document.parameters); + render_parameter_table( + writer, + "Outputs", + match self.document.returns { docs::Returns::Scalar(field) => std::slice::from_ref(field), docs::Returns::Record { fields, .. } => fields, }, - } - .render(writer); + ); for note in self.document.notes { Admonition { kind: "note", @@ -146,32 +142,72 @@ impl Render for FunctionSection<'_> { } } -struct ParameterTable<'a> { - title: &'a str, - parameters: &'a [Parameter], +trait CatalogParameter { + fn name(&self) -> &str; + fn unit(&self) -> &str; + fn domain(&self) -> Option<&str>; + fn description(&self) -> &str; } -impl Render for ParameterTable<'_> { - fn render(&self, writer: &mut Writer) { - writer.write(format_args!( - "#### {}\n\n| Name | Unit | Domain | Description |\n| --- | --- | --- | --- |\n", - self.title +impl CatalogParameter for Parameter { + fn name(&self) -> &str { + &self.name + } + fn unit(&self) -> &str { + &self.unit + } + fn domain(&self) -> Option<&str> { + self.domain.as_deref() + } + fn description(&self) -> &str { + &self.description + } +} + +fn render_input_table(writer: &mut Writer, inputs: &[crate::model::Input]) { + writer.write( + "#### Inputs\n\n| Name | Type | Unit | Domain | Description |\n| --- | --- | --- | --- | --- |\n", + ); + for input in inputs { + let value_type = input + .enum_type() + .map(|definition| format!("`{}`", definition.name)) + .unwrap_or_else(|| "`number`".to_owned()); + writer.line(format_args!( + "| `{}` | {} | {} | {} | {} |", + input.name(), + value_type, + input + .unit() + .map(escape_table) + .unwrap_or_else(|| "\u{2014}".into()), + input + .domain() + .map(escape_table) + .unwrap_or_else(|| "\u{2014}".into()), + escape_table(input.description()), + )); + } + writer.blank_line(); +} + +fn render_parameter_table(writer: &mut Writer, title: &str, parameters: &[impl CatalogParameter]) { + writer.write(format_args!( + "#### {title}\n\n| Name | Unit | Domain | Description |\n| --- | --- | --- | --- |\n" + )); + for parameter in parameters { + writer.line(format_args!( + "| `{}` | {} | {} | {} |", + parameter.name(), + escape_table(parameter.unit()), + parameter + .domain() + .map(escape_table) + .unwrap_or_else(|| "\u{2014}".into()), + escape_table(parameter.description()), )); - for parameter in self.parameters { - writer.line(format_args!( - "| `{}` | {} | {} | {} |", - parameter.name, - escape_table(¶meter.unit), - parameter - .domain - .as_deref() - .map(escape_table) - .unwrap_or_else(|| "\u{2014}".into()), - escape_table(¶meter.description), - )); - } - writer.blank_line(); } + writer.blank_line(); } struct Admonition<'a> { diff --git a/codegen/src/targets/mod.rs b/codegen/src/targets/mod.rs index ebdb804..160a6db 100644 --- a/codegen/src/targets/mod.rs +++ b/codegen/src/targets/mod.rs @@ -12,10 +12,71 @@ use anyhow::Result; use crate::{ compile, - model::{CompiledFunction, Entry}, + model::{CompiledFunction, Entry, Output as FunctionOutput}, output::{self, Output}, + semantic::VariableValue, }; +pub(super) fn record_types( + functions: &[&CompiledFunction], +) -> Result>> { + let mut records = BTreeMap::new(); + for function in functions { + if let FunctionOutput::Struct(fields) = &function.core.output { + let name = function.entry.spec.functions[function.function_index] + .result_class() + .expect("record output has a result class"); + insert_record_type(&mut records, name, fields)?; + } + for lookup in function.ir.variables.iter().filter_map(|variable| { + if let VariableValue::RecordLookup(lookup) = &variable.value { + Some(lookup) + } else { + None + } + }) { + insert_record_type(&mut records, &lookup.output.name, &lookup.output.fields)?; + } + } + Ok(records) +} + +fn insert_record_type( + records: &mut BTreeMap>, + name: &str, + fields: &[String], +) -> Result<()> { + if let Some(previous) = records.get(name) + && previous != fields + { + anyhow::bail!("record type `{name}` has conflicting field definitions"); + } + records.insert(name.to_owned(), fields.to_vec()); + Ok(()) +} + +#[cfg(test)] +pub(crate) fn render_rust_for_test( + functions: &[CompiledFunction], +) -> Result> { + rust::render(functions) +} + +#[cfg(test)] +pub(crate) fn render_native_for_test( + functions: &[CompiledFunction], +) -> Result<(Vec, Vec)> { + let rendered = native::render(functions)?; + Ok((rendered.c_headers, rendered.cpp_modules)) +} + +#[cfg(test)] +pub(crate) fn render_python_extension_for_test( + functions: &[CompiledFunction], +) -> Result> { + Ok(python::render(functions)?.extension) +} + pub(super) fn group_by_source( functions: &[CompiledFunction], ) -> BTreeMap<&str, Vec<&CompiledFunction>> { diff --git a/codegen/src/targets/native.rs b/codegen/src/targets/native.rs index ab2c24f..523210a 100644 --- a/codegen/src/targets/native.rs +++ b/codegen/src/targets/native.rs @@ -5,14 +5,17 @@ use convert_case::{Boundary, Case, Casing}; use crate::{ documentation::{self as docs, FunctionDocument, SourceDocument}, - model::{CompiledFunction, Function, Output, Parameter, Scope, Source}, + model::{ + CompiledFunction, CompiledInput, EnumDefinition, Function, Output, Parameter, Scope, Source, + }, render::{Render, Writer}, + semantic::{RecordLookup, Reference, ResultBinding, VariableValue}, }; use crate::{ output::GeneratedFile, render::c::{self, Dialect}, - targets::group_by_source, + targets::{group_by_source, record_types}, }; pub(super) const HEADER: &str = "/* @generated by ptfkit-codegen; DO NOT EDIT. */\n"; @@ -82,6 +85,9 @@ fn c_header(slug: &str, functions: &[&CompiledFunction]) -> Result { if requires_pow4(functions) { writer.write("#include \n"); } + if requires_record_literal(functions) { + writer.write("#include \n"); + } if requires_math(functions) { writer.write("#include \n"); } @@ -92,6 +98,10 @@ fn c_header(slug: &str, functions: &[&CompiledFunction]) -> Result { .first() .expect("generated source contains at least one function"); source_comment(&first.entry.spec.source, &first.entry.spec.scope).render(&mut writer); + for definition in enum_definitions(functions) { + writer.blank_line(); + render_enum(&mut writer, slug, definition, NativeDialect::C); + } let mut schemas = BTreeSet::new(); for function in functions { let spec = &function.entry.spec.functions[function.function_index]; @@ -99,7 +109,7 @@ fn c_header(slug: &str, functions: &[&CompiledFunction]) -> Result { let schema = spec .result_class() .ok_or_else(|| anyhow::anyhow!("record output has no result class"))?; - if schemas.insert(schema) { + if schemas.insert(schema.to_owned()) { writer.blank_line(); render_struct( &mut writer, @@ -111,6 +121,21 @@ fn c_header(slug: &str, functions: &[&CompiledFunction]) -> Result { } } } + for (name, fields) in record_types(functions)? { + if schemas.insert(name.clone()) { + writer.blank_line(); + render_internal_struct( + &mut writer, + &fields, + &c_result_name(&name), + NativeDialect::C, + ); + } + } + for lookup in lookup_definitions(functions) { + writer.blank_line(); + render_record_lookup_helper(&mut writer, slug, lookup, NativeDialect::C); + } for function in functions { writer.blank_line(); NativeFunction::c(function)?.render(&mut writer); @@ -122,7 +147,7 @@ fn c_header(slug: &str, functions: &[&CompiledFunction]) -> Result { fn cpp_module(slug: &str, functions: &[&CompiledFunction]) -> Result { let mut writer = Writer::new(); writer.write(format_args!("{HEADER}\n\n")); - if requires_pow4(functions) || requires_math(functions) { + if requires_pow4(functions) || requires_math(functions) || requires_cpp_unreachable(functions) { writer.write("module;\n"); if requires_pow4(functions) { writer.write("#include \n"); @@ -130,6 +155,9 @@ fn cpp_module(slug: &str, functions: &[&CompiledFunction]) -> Result { if requires_math(functions) { writer.write("#include \n"); } + if requires_cpp_unreachable(functions) { + writer.write("#include \n"); + } writer.blank_line(); } writer.write(format_args!("export module ptfkit.{slug};\n\n")); @@ -142,8 +170,13 @@ fn cpp_module(slug: &str, functions: &[&CompiledFunction]) -> Result { .iter() .map(|function| NativeFunction::cpp(function)) .collect::>>()?; + let record_types = record_types(functions)?; writer.line(format_args!("export namespace ptfkit::{slug} {{")); writer.indented(|writer| { + for definition in enum_definitions(functions) { + writer.blank_line(); + render_enum(writer, slug, definition, NativeDialect::Cpp); + } let mut schemas = BTreeSet::new(); for function in functions { let spec = &function.entry.spec.functions[function.function_index]; @@ -151,12 +184,22 @@ fn cpp_module(slug: &str, functions: &[&CompiledFunction]) -> Result { let result = spec .result_class() .expect("record output has a result class"); - if schemas.insert(result) { + if schemas.insert(result.to_owned()) { writer.blank_line(); render_struct(writer, fields, spec, result, NativeDialect::Cpp); } } } + for (name, fields) in &record_types { + if schemas.insert(name.clone()) { + writer.blank_line(); + render_internal_struct(writer, fields, name, NativeDialect::Cpp); + } + } + for lookup in lookup_definitions(functions) { + writer.blank_line(); + render_record_lookup_helper(writer, slug, lookup, NativeDialect::Cpp); + } for function in &native_functions { writer.blank_line(); function.render(writer); @@ -203,11 +246,9 @@ impl<'a> NativeFunction<'a> { }; let output_name = &spec.outputs.fields()[0].name; let terminal = matches!(function.core.output, Output::Scalar) - && function - .ir - .variables - .last() - .is_some_and(|variable| variable.name == *output_name); + && function.ir.variables.last().is_some_and(|variable| { + variable.name == *output_name && matches!(variable.value, VariableValue::Number(_)) + }); Ok(Self { function, result, @@ -235,7 +276,18 @@ impl Render for NativeFunction<'_> { if index > 0 { writer.write(", "); } - writer.write("double "); + let parameter = &spec.inputs[index]; + match (parameter.enum_type(), self.dialect) { + (Some(definition), NativeDialect::C) => { + writer.write(c_enum_name(&self.function.entry.slug, &definition.name)); + writer.write(" "); + } + (Some(definition), NativeDialect::Cpp) => { + writer.write(&definition.name); + writer.write(" "); + } + (None, _) => writer.write("double "), + } writer.write(input); } writer.line(") {"); @@ -247,14 +299,21 @@ impl Render for NativeFunction<'_> { .iter() .take(self.function.ir.variables.len() - usize::from(self.terminal)) { - writer.write(format_args!("const double {} = ", variable.name)); - writer.write(c::expression( - &variable.expression, - &self.function.core.inputs, - &self.function.ir.variables, - self.expression_dialect(), - )); - writer.line(";"); + match &variable.value { + VariableValue::Number(expression) => { + writer.write(format_args!("const double {} = ", variable.name)); + writer.write(c::expression( + expression, + &self.function.core.inputs, + &self.function.ir.variables, + self.expression_dialect(), + )); + writer.line(";"); + } + VariableValue::RecordLookup(lookup) => { + self.render_record_lookup(writer, &variable.name, lookup); + } + } } self.render_return(writer); }); @@ -271,6 +330,15 @@ impl NativeFunction<'_> { } fn render_return(&self, writer: &mut Writer) { + if matches!(self.function.core.output, Output::Struct(_)) + && let ResultBinding::RecordVariable(index) = self.function.ir.result + { + writer.line(format_args!( + "return {};", + self.function.ir.variables[index].name + )); + return; + } let output_name = &self.function.entry.spec.functions[self.function.function_index] .outputs .fields()[0] @@ -279,13 +347,14 @@ impl NativeFunction<'_> { Output::Scalar if self.terminal => { writer.write("return "); writer.write(c::expression( - &self - .function + self.function .ir .variables .last() .expect("terminal variable") - .expression, + .value + .as_number() + .expect("terminal scalar variable is numeric"), &self.function.core.inputs, &self.function.ir.variables, self.expression_dialect(), @@ -315,6 +384,123 @@ impl NativeFunction<'_> { } } } + + fn render_record_lookup(&self, writer: &mut Writer, name: &str, lookup: &RecordLookup) { + let result = match self.dialect { + NativeDialect::C => c_result_name(&lookup.output.name), + NativeDialect::Cpp => lookup.output.name.clone(), + }; + let key = match lookup.key { + Reference::Input(index) => &self.function.core.inputs[index], + Reference::Variable(index) => &self.function.ir.variables[index].name, + }; + writer.line(format_args!( + "const {result} {name} = {}({key});", + record_lookup_function_name(lookup, self.dialect) + )); + } +} + +fn lookup_definitions<'a>(functions: &[&'a CompiledFunction]) -> Vec<&'a RecordLookup> { + let mut names = BTreeSet::new(); + functions + .iter() + .flat_map(|function| &function.ir.variables) + .filter_map(|variable| match &variable.value { + VariableValue::RecordLookup(lookup) => Some(lookup), + VariableValue::Number(_) => None, + }) + .filter(|lookup| names.insert((lookup.enum_name.clone(), lookup.output.name.clone()))) + .collect() +} + +fn record_lookup_function_name(lookup: &RecordLookup, _dialect: NativeDialect) -> String { + format!( + "{}_from_{}", + c_result_name(&lookup.output.name), + lookup.enum_name.to_case(Case::Snake) + ) +} + +fn render_record_lookup_helper( + writer: &mut Writer, + slug: &str, + lookup: &RecordLookup, + dialect: NativeDialect, +) { + let result = match dialect { + NativeDialect::C => c_result_name(&lookup.output.name), + NativeDialect::Cpp => lookup.output.name.clone(), + }; + let enum_name = match dialect { + NativeDialect::C => c_enum_name(slug, &lookup.enum_name), + NativeDialect::Cpp => lookup.enum_name.clone(), + }; + match dialect { + NativeDialect::C => writer.write("static inline "), + NativeDialect::Cpp => writer.write("[[nodiscard]] inline "), + } + writer.line(format_args!( + "{result} {}({enum_name} value) {{", + record_lookup_function_name(lookup, dialect) + )); + writer.indented(|writer| { + writer.line("switch (value) {"); + writer.indented(|writer| { + for case in &lookup.cases { + let member = match dialect { + NativeDialect::C => c_enum_member(slug, &lookup.enum_name, &case.member), + NativeDialect::Cpp => format!( + "{}::{}", + lookup.enum_name, + case.member.to_case(Case::Pascal) + ), + }; + writer.line(format_args!("case {member}:")); + writer.indented(|writer| { + writer.write("return "); + render_record_literal(writer, &result, &case.values, dialect); + writer.line(";"); + }); + } + writer.line("default:"); + writer.indented(|writer| match dialect { + NativeDialect::C => { + let nan_values = std::iter::repeat_n("NAN", lookup.output.fields.len()) + .collect::>() + .join(", "); + writer.line(format_args!( + "return PTFKIT_RECORD_LITERAL({result}, {nan_values});" + )); + } + NativeDialect::Cpp => writer.line("std::unreachable();"), + }); + }); + writer.line("}"); + }); + writer.line("}"); +} + +fn render_record_literal( + writer: &mut Writer, + result: &str, + values: &[crate::semantic::Number], + dialect: NativeDialect, +) { + match dialect { + NativeDialect::C => writer.write(format_args!("PTFKIT_RECORD_LITERAL({result}, ")), + NativeDialect::Cpp => writer.write(format_args!("{result}{{")), + } + for (index, value) in values.iter().enumerate() { + if index > 0 { + writer.write(", "); + } + writer.write(c::float_literal(&value.lexeme)); + } + writer.write(match dialect { + NativeDialect::C => ")", + NativeDialect::Cpp => "}", + }); } fn render_values(writer: &mut Writer, values: &[String]) { @@ -355,6 +541,88 @@ fn render_struct( } } +fn render_internal_struct( + writer: &mut Writer, + fields: &[String], + name: &str, + dialect: NativeDialect, +) { + match dialect { + NativeDialect::C => writer.line("typedef struct {"), + NativeDialect::Cpp => writer.line(format_args!("struct {name} {{")), + } + writer.indented(|writer| { + for field in fields { + writer.line(format_args!("double {field};")); + } + }); + match dialect { + NativeDialect::C => writer.line(format_args!("}} {name};")), + NativeDialect::Cpp => writer.line("};"), + } +} + +fn enum_definitions<'a>(functions: &[&'a CompiledFunction]) -> Vec<&'a EnumDefinition> { + let mut names = BTreeSet::new(); + functions + .iter() + .flat_map(|function| { + function.entry.spec.functions[function.function_index] + .inputs + .iter() + .filter_map(|input| input.enum_type()) + }) + .filter(|definition| names.insert(definition.name.as_str())) + .collect() +} + +pub(crate) fn c_enum_name(module: &str, name: &str) -> String { + format!("{module}_{}", name.to_case(Case::Snake)) +} + +pub(crate) fn c_enum_member(module: &str, enum_name: &str, member_name: &str) -> String { + format!( + "{module}_{}_{}", + enum_name.to_case(Case::Snake), + member_name + ) +} + +fn render_enum( + writer: &mut Writer, + module: &str, + definition: &EnumDefinition, + dialect: NativeDialect, +) { + match dialect { + NativeDialect::C => writer.line("typedef enum {"), + NativeDialect::Cpp => writer.line(format_args!("enum class {} {{", definition.name)), + } + writer.indented(|writer| { + for member in &definition.values { + if let Some(description) = &member.description { + Comment(vec![format!("@brief {description}")]).render(writer); + } + match dialect { + NativeDialect::C => writer.line(format_args!( + "{},", + c_enum_member(module, &definition.name, &member.name) + )), + NativeDialect::Cpp => { + writer.line(format_args!("{},", member.name.to_case(Case::Pascal))) + } + } + } + }); + match dialect { + NativeDialect::C => writer.line(format_args!( + "}} {};", + c_enum_name(module, &definition.name) + )), + NativeDialect::Cpp => writer.line("};"), + } +} + fn source_comment(source: &Source, scope: &Scope) -> Comment { source_comment_from_document(docs::for_source(source, scope)) } @@ -395,7 +663,7 @@ fn function_comment_from_document(document: FunctionDocument<'_>) -> Comment { lines.extend(document.parameters.iter().map(|parameter| { format!( "@param {} {}", - parameter.name, + parameter.name(), docs::parameter_details(parameter) ) })); @@ -488,7 +756,10 @@ fn requires_math(functions: &[&CompiledFunction]) -> bool { .ir .variables .iter() - .any(|variable| c::requires_math(&variable.expression)) + .any(|variable| match &variable.value { + VariableValue::Number(expression) => c::requires_math(expression), + VariableValue::RecordLookup(_) => true, + }) }) } @@ -498,10 +769,25 @@ fn requires_pow4(functions: &[&CompiledFunction]) -> bool { .ir .variables .iter() - .any(|variable| c::requires_pow4(&variable.expression)) + .filter_map(|variable| variable.value.as_number()) + .any(c::requires_pow4) }) } +fn requires_record_literal(functions: &[&CompiledFunction]) -> bool { + functions.iter().any(|function| { + function + .ir + .variables + .iter() + .any(|variable| matches!(variable.value, VariableValue::RecordLookup(_))) + }) +} + +fn requires_cpp_unreachable(functions: &[&CompiledFunction]) -> bool { + requires_record_literal(functions) +} + fn c_test(slug: &str, functions: &[&CompiledFunction]) -> Result { c_compatibility_test(slug, functions) } @@ -532,7 +818,7 @@ fn c_compatibility_test(slug: &str, functions: &[&CompiledFunction]) -> Result Result { writer.line("{"); writer.indented(|writer| { writer.write(format_args!("const auto result = ptfkit::{slug}::{}(", function.core.name)); - render_literals(writer, &case.inputs); + render_literals(writer, &case.inputs, NativeDialect::Cpp, Some(slug)); writer.line(");"); if matches!(function.core.output, Output::Struct(_)) { let result = spec @@ -611,12 +897,36 @@ fn module_test(slug: &str, functions: &[&CompiledFunction]) -> Result { Ok(writer.into_string()) } -fn render_literals(writer: &mut Writer, values: &[f64]) { +fn render_literals( + writer: &mut Writer, + values: &[CompiledInput], + dialect: NativeDialect, + cpp_slug: Option<&str>, +) { for (index, value) in values.iter().enumerate() { if index > 0 { writer.write(", "); } - writer.write(c::test_float_literal(*value)); + match value { + CompiledInput::Number(value) => writer.write(c::test_float_literal(*value)), + CompiledInput::Enum { + enum_name, + member_name, + .. + } => match dialect { + NativeDialect::C => writer.write(c_enum_member( + cpp_slug.expect("C test literals have a source module"), + enum_name, + member_name, + )), + NativeDialect::Cpp => writer.write(format_args!( + "ptfkit::{}::{}::{}", + cpp_slug.expect("C++ test literals have a source namespace"), + enum_name, + member_name.to_case(Case::Pascal) + )), + }, + } } } @@ -667,4 +977,12 @@ mod tests { "dharumarajan2019_water_retention_result" ); } + + #[test] + fn c_enum_members_include_the_source_module() { + assert_eq!( + c_enum_member("clapp1978", "UsdaTextureClass", "sand"), + "clapp1978_usda_texture_class_sand" + ); + } } diff --git a/codegen/src/targets/python/extension.rs b/codegen/src/targets/python/extension.rs index 0780dfc..e804f90 100644 --- a/codegen/src/targets/python/extension.rs +++ b/codegen/src/targets/python/extension.rs @@ -26,7 +26,7 @@ pub(crate) fn render(functions: &[CompiledFunction]) -> Result Result Result { let name = &function.core.name; let inputs = &function.core.inputs; + let specification = &function.entry.spec.functions[function.function_index]; let values = match &function.core.output { Output::Scalar => vec![ function.entry.spec.functions[function.function_index] @@ -89,6 +90,22 @@ fn ufunc(function: &CompiledFunction) -> Result { Output::Struct(fields) => fields.clone(), }; let mut writer = Writer::new(); + writer.line(format_args!( + "static const int {name}_types[] = {{{}}};", + specification + .inputs + .iter() + .map(|input| { + if input.enum_type().is_some() { + "NPY_UINT32" + } else { + "NPY_DOUBLE" + } + }) + .chain(std::iter::repeat_n("NPY_DOUBLE", values.len())) + .collect::>() + .join(", "), + )); writer.line(format_args!( "static int {name}_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) {{" )); @@ -97,8 +114,13 @@ fn ufunc(function: &CompiledFunction) -> Result { writer.line("(void)strides;"); writer.line("(void)transferdata;"); for (index, input) in inputs.iter().enumerate() { + let value_type = if specification.inputs[index].enum_type().is_some() { + "npy_uint32" + } else { + "double" + }; writer.line(format_args!( - "const double *in_{input} = (const double *)data[{index}];" + "const {value_type} *in_{input} = (const {value_type} *)data[{index}];" )); } for (index, value) in values.iter().enumerate() { @@ -110,7 +132,18 @@ fn ufunc(function: &CompiledFunction) -> Result { writer.line("for (npy_intp index = 0; index < dimensions[0]; index++) {"); writer.indented(|writer| { for input in inputs { - writer.line(format_args!("const double {input} = in_{input}[index];")); + let input_index = inputs + .iter() + .position(|item| item == input) + .expect("input exists"); + let value_type = if specification.inputs[input_index].enum_type().is_some() { + "npy_uint32" + } else { + "double" + }; + writer.line(format_args!( + "const {value_type} {input} = in_{input}[index];" + )); } render_kernel_call(writer, function, inputs, &values, Some("[index]")); }); @@ -128,9 +161,12 @@ fn ufunc(function: &CompiledFunction) -> Result { writer.line("for (npy_intp index = 0; index < dimensions[0]; index++) {"); writer.indented(|writer| { for (index, input) in inputs.iter().enumerate() { - writer.line(format_args!( - "const double {input} = *(const double *)(data[{index}] + index * strides[{index}]);" - )); + let value_type = if specification.inputs[index].enum_type().is_some() { + "npy_uint32" + } else { + "double" + }; + writer.line(format_args!("const {value_type} {input} = *(const {value_type} *)(data[{index}] + index * strides[{index}]);")); } render_kernel_call(writer, function, inputs, &values, None); }); diff --git a/codegen/src/targets/python/test.rs b/codegen/src/targets/python/test.rs index 6523051..40b415d 100644 --- a/codegen/src/targets/python/test.rs +++ b/codegen/src/targets/python/test.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use crate::{ - model::{CompiledFunction, Function, Outputs, PythonGeneration}, + model::{CompiledFunction, Function, GoldenInput, Outputs, PythonGeneration}, output::GeneratedFile, }; @@ -39,6 +39,11 @@ fn module_source(slug: &str, functions: &[&CompiledFunction]) -> String { if let Some(result_class) = function.result_class() { imports.push(result_class); } + for input in &function.inputs { + if let Some(enum_type) = input.enum_type() { + imports.push(&enum_type.name); + } + } } imports.sort_by_key(|name| natural_sort_key(name)); imports.dedup(); @@ -69,8 +74,8 @@ fn function_source(module: &mut Module, function: &Function) { for case in &function.golden_tests { writer.line(format_args!( "({}, {}, {}, {}),", - dictionary(&case.inputs), - dictionary(&case.expected), + dictionary(&case.inputs, function), + numeric_dictionary(&case.expected), float(case.rtol), float(case.atol), )); @@ -80,14 +85,33 @@ fn function_source(module: &mut Module, function: &Function) { module.blank_line(); module.blank_line(); let name = &function.public_api.name; + let input_value_type = if function + .inputs + .iter() + .any(|input| input.enum_type().is_some()) + { + "object" + } else { + "float" + }; module.line(format_args!( "@pytest.mark.parametrize(('inputs', 'expected', 'rtol', 'atol'), {cases_name})" )); module.line(format_args!( - "def test_{name}_golden(inputs: dict[str, float], expected: dict[str, float], rtol: float, atol: float):" + "def test_{name}_golden(inputs: dict[str, {input_value_type}], expected: dict[str, float], rtol: float, atol: float):" )); module.indented(|writer| { - writer.line(format_args!("result = {name}(**inputs)")); + if function + .inputs + .iter() + .any(|input| input.enum_type().is_some()) + { + writer.line(format_args!( + "result = {name}(**inputs) # ty: ignore[no-matching-overload]" + )); + } else { + writer.line(format_args!("result = {name}(**inputs)")); + } writer.blank_line(); render_expected_assertion(writer, function, ""); }); @@ -152,7 +176,32 @@ fn render_out_assertion(writer: &mut crate::render::Writer, function: &Function) } } -fn dictionary(values: &BTreeMap) -> String { +fn dictionary(values: &BTreeMap, function: &Function) -> String { + let entries = values + .iter() + .map(|(name, value)| { + let value = match value { + GoldenInput::Number(value) => float(*value), + GoldenInput::Enum(member) => { + let enum_name = function + .inputs + .iter() + .find(|input| input.name() == name) + .and_then(|input| input.enum_type()) + .expect("enum golden input has a resolved enum type") + .name + .as_str(); + format!("{enum_name}.{}", member.to_ascii_uppercase()) + } + }; + format!("'{name}': {value}") + }) + .collect::>() + .join(", "); + format!("{{{entries}}}") +} + +fn numeric_dictionary(values: &BTreeMap) -> String { let entries = values .iter() .map(|(name, value)| format!("'{name}': {}", float(*value))) diff --git a/codegen/src/targets/python/wrapper.rs b/codegen/src/targets/python/wrapper.rs index 6078a4c..da61d46 100644 --- a/codegen/src/targets/python/wrapper.rs +++ b/codegen/src/targets/python/wrapper.rs @@ -4,7 +4,7 @@ use anyhow::{Result, bail}; use crate::{ documentation::{self as docs, FunctionDocument}, - model::{CompiledFunction, Function, Parameter, PythonGeneration, Scope, Source}, + model::{CompiledFunction, EnumDefinition, Function, PythonGeneration, Scope, Source}, output::GeneratedFile, render::Writer, }; @@ -22,6 +22,26 @@ struct PythonFunction<'a> { keyword_inputs: Vec, parameters: String, docstring: PythonDocstring, + enum_inputs: Vec, +} + +struct PythonEnumInput { + input: String, + enum_name: String, +} + +#[derive(Clone)] +struct PythonEnum { + name: String, + description: String, + members: Vec, +} + +#[derive(Clone)] +struct PythonEnumMember { + name: String, + value: String, + description: Option, } #[derive(PartialEq, Eq)] @@ -77,6 +97,16 @@ pub(crate) fn render(functions: &[CompiledFunction]) -> Result::new(); + for resolved in &functions { + for input in &resolved.entry.spec.functions[resolved.function_index].inputs { + if let Some(definition) = input.enum_type() { + enums + .entry(definition.name.clone()) + .or_insert_with(|| python_enum(definition)); + } + } + } let functions = functions .iter() .map(|resolved| view(resolved)) @@ -85,9 +115,17 @@ pub(crate) fn render(functions: &[CompiledFunction]) -> Result>(); exports.sort_by_key(|export| natural_sort_key(export)); + exports.dedup(); let classes = classes.into_values().collect::>(); + let enums = enums.into_values().collect::>(); let typing_imports = if classes.is_empty() { "TYPE_CHECKING, overload" } else { @@ -98,6 +136,7 @@ pub(crate) fn render(functions: &[CompiledFunction]) -> Result], exports: &[String], ) -> String { @@ -137,9 +177,11 @@ fn module_source( } module.blank_line(); render_module_docstring(&mut module, source, scope); - module.write(format_args!( - "from __future__ import annotations\n\nfrom typing import {typing_imports}\n\n" - )); + module.write("from __future__ import annotations\n\n"); + if !enums.is_empty() { + module.write("from enum import Enum\n"); + } + module.write(format_args!("from typing import {typing_imports}\n\n")); module.line("from ptfkit._dispatch import call as _call"); module.block( "from ptfkit._ptfkit import (", @@ -153,11 +195,37 @@ fn module_source( }, ")", ); + if !enums.is_empty() { + module.line("from ptfkit.enums import EnumArray"); + } module.line("\n\nif TYPE_CHECKING:"); module.indented(|writer| { - writer.line("from numpy import floating"); - writer.line("from numpy.typing import ArrayLike, NDArray"); + if !enums.is_empty() { + writer.line("from collections.abc import Iterable"); + writer.blank_line(); + } + writer.line(if enums.is_empty() { + "from numpy import floating" + } else { + "from numpy import floating, uint32" + }); + let has_numeric_inputs = functions + .iter() + .any(|function| function.enum_inputs.len() < function.scalar_inputs.len()); + writer.line(if has_numeric_inputs { + "from numpy.typing import ArrayLike, NDArray" + } else { + "from numpy.typing import NDArray" + }); }); + for enum_definition in enums { + render_enum(&mut module, enum_definition); + } + for function in functions { + for enum_input in &function.enum_inputs { + render_enum_encoder(&mut module, function.name, enum_input); + } + } if !classes.is_empty() { module.blank_line(); module.assignment("T", "TypeVar('T')"); @@ -255,30 +323,178 @@ fn render_function(module: &mut Module, function: &PythonFunction<'_>) { fn view(resolved: &CompiledFunction) -> PythonFunction<'_> { let function = &resolved.entry.spec.functions[resolved.function_index]; + let enum_inputs = function + .inputs + .iter() + .filter_map(|input| { + input.enum_type().map(|definition| PythonEnumInput { + input: input.name().to_owned(), + enum_name: definition.name.clone(), + }) + }) + .collect::>(); let names = function .inputs .iter() - .map(|input| input.name.clone()) + .map(|input| input.name().to_owned()) .collect::>(); PythonFunction { name: &function.public_api.name, rust_name: &resolved.core.name, result_class: function.result_class(), - scalar_inputs: names.iter().map(|name| format!("{name}: float")).collect(), + scalar_inputs: names + .iter() + .map( + |name| match enum_inputs.iter().find(|input| input.input == **name) { + Some(input) => format!("{name}: {}", input.enum_name), + None => format!("{name}: float"), + }, + ) + .collect(), array_inputs: names .iter() - .map(|name| format!("{name}: ArrayLike")) + .map( + |name| match enum_inputs.iter().find(|input| input.input == **name) { + Some(input) => format!("{name}: EnumArray[{}]", input.enum_name), + None => format!("{name}: ArrayLike"), + }, + ) .collect(), keyword_inputs: function .inputs .iter() - .map(|input| format!("{}: float | ArrayLike,", input.name)) + .map( + |input| match enum_inputs.iter().find(|item| item.input == input.name()) { + Some(item) => format!( + "{}: {} | EnumArray[{}],", + input.name(), + item.enum_name, + item.enum_name + ), + None => format!("{}: float | ArrayLike,", input.name()), + }, + ) .collect(), - parameters: names.join(", "), + parameters: names + .iter() + .map( + |name| match enum_inputs.iter().find(|input| input.input == **name) { + Some(_) => format!("_encode_{}_{}({name})", function.public_api.name, name), + None => name.clone(), + }, + ) + .collect::>() + .join(", "), docstring: function_docstring(function), + enum_inputs, } } +fn python_enum(definition: &EnumDefinition) -> PythonEnum { + PythonEnum { + name: definition.name.clone(), + description: definition.description.clone(), + members: definition + .values + .iter() + .map(|member| PythonEnumMember { + name: enum_member(&member.name), + value: member.value.clone(), + description: member.description.clone(), + }) + .collect(), + } +} + +fn render_enum(module: &mut Module, definition: &PythonEnum) { + module.blank_line(); + module.blank_line(); + module.line(format_args!("class {}(Enum):", definition.name)); + module.indented(|writer| { + writer.line(format_args!("\"\"\"{}", definition.description)); + if definition + .members + .iter() + .any(|member| member.description.is_some()) + { + writer.blank_line(); + writer.line("Attributes:"); + for member in &definition.members { + if let Some(description) = &member.description { + writer.line(format_args!(" {}: {description}", member.name)); + } + } + } + writer.blank_line(); + writer.line("\"\"\""); + writer.blank_line(); + for member in &definition.members { + writer.line(format_args!("{} = {:?}", member.name, member.value)); + } + writer.blank_line(); + writer.line("@classmethod"); + writer.line(format_args!( + "def array(cls, values: Iterable[{}]) -> EnumArray[{}]:", + definition.name, definition.name + )); + writer.indented(|writer| { + writer.line("\"\"\"Encode members once as a reusable typed enum array.\"\"\""); + writer.line("return EnumArray._from_members(cls, values) # noqa: SLF001") + }); + }); +} + +fn render_enum_encoder(module: &mut Module, function_name: &str, enum_input: &PythonEnumInput) { + module.blank_line(); + module.blank_line(); + let encoder = format!("_encode_{function_name}_{}", enum_input.input); + module.line(format_args!( + "def {encoder}(value: {} | EnumArray[{}]) -> uint32 | NDArray[uint32]:", + enum_input.enum_name, enum_input.enum_name + )); + module.indented(|writer| { + writer.line(format_args!( + "if isinstance(value, {}):", + enum_input.enum_name + )); + writer.indented(|writer| { + writer.line(format_args!( + "return EnumArray._encode_member({}, value) # noqa: SLF001", + enum_input.enum_name + )); + }); + writer.line("if isinstance(value, EnumArray):"); + writer.indented(|writer| { + writer.line(format_args!( + "return value._codes_for({}) # noqa: SLF001", + enum_input.enum_name + )); + }); + writer.line(format_args!( + "message = 'expected {} or EnumArray[{}]'", + enum_input.enum_name, enum_input.enum_name + )); + writer.line("raise TypeError(message)"); + }); +} + +fn enum_member(label: &str) -> String { + label + .chars() + .map(|character| { + if character.is_ascii_alphanumeric() { + character.to_ascii_uppercase() + } else { + '_' + } + }) + .collect::() + .split('_') + .filter(|part| !part.is_empty()) + .collect::>() + .join("_") +} + fn render_module_docstring(module: &mut Module, source: &Source, scope: &Scope) { let document = docs::for_source(source, scope); module.line(format_args!("r\"\"\"{}", document.summary)); @@ -379,7 +595,7 @@ fn result_class_docstring(function: &Function) -> PythonDocstring { } } -fn parameter_documentation(parameter: &Parameter) -> String { +fn parameter_documentation(parameter: &impl docs::ParameterMetadata) -> String { docs::parameter_documentation(parameter) } @@ -478,7 +694,9 @@ fn wrap_doc_line(text: &str, first_width: usize, continuation_width: usize) -> V #[cfg(test)] mod tests { - use super::{function_docstring, render_module_docstring}; + use super::{ + PythonEnum, PythonEnumMember, function_docstring, render_enum, render_module_docstring, + }; use crate::{ model::{Documentation, Function, FunctionScope, Models, PublicApi, Scope, Source}, targets::python::syntax::Module, @@ -553,4 +771,24 @@ mod tests { ); assert!(docstring.contains("[DOI: 10.1234/test](https://example.test/doi/10.1234/test)")); } + + #[test] + fn renders_enum_and_member_descriptions_as_attributes() { + let definition = PythonEnum { + name: "TestCategory".into(), + description: "Test category type.".into(), + members: vec![PythonEnumMember { + name: "FIRST".into(), + value: "first".into(), + description: Some("First test category.".into()), + }], + }; + + let mut module = Module::new(""); + render_enum(&mut module, &definition); + let rendered = module.into_string(); + + assert!(rendered.contains("\"\"\"Test category type.")); + assert!(rendered.contains("Attributes:\n FIRST: First test category.")); + } } diff --git a/codegen/src/targets/reference/c.rs b/codegen/src/targets/reference/c.rs index 2f7f24e..1308f57 100644 --- a/codegen/src/targets/reference/c.rs +++ b/codegen/src/targets/reference/c.rs @@ -5,10 +5,13 @@ use anyhow::Result; use crate::render::markdown::HEADER; use crate::{ documentation::{self as docs}, - model::{CompiledFunction, Output, Parameter}, + model::{CompiledFunction, EnumDefinition, Output, Parameter}, output::GeneratedFile, render::{Writer, markdown}, - targets::{group_by_source, native::c_result_name}, + targets::{ + group_by_source, + native::{c_enum_member, c_enum_name, c_result_name}, + }, }; pub(crate) fn render(functions: &[CompiledFunction]) -> Result> { @@ -142,6 +145,16 @@ fn render_header(writer: &mut Writer, slug: &str, functions: &[&CompiledFunction "[PTF catalog page](../../../ptf-catalog/sources/{slug}.md)\n\n" )); + let mut enums = BTreeSet::new(); + for function in functions { + for input in &spec(function).inputs { + if let Some(definition) = input.enum_type() + && enums.insert(definition.name.clone()) + { + render_enum(writer, slug, definition, true); + } + } + } let mut structures = BTreeSet::new(); for function in functions { if let Output::Struct(_) = &function.core.output { @@ -162,6 +175,52 @@ fn render_header(writer: &mut Writer, slug: &str, functions: &[&CompiledFunction Ok(()) } +fn render_enum(writer: &mut Writer, module: &str, definition: &EnumDefinition, c: bool) { + let name = if c { + c_enum_name(module, &definition.name) + } else { + definition.name.clone() + }; + writer.write(format_args!("## `{name}`\n\n")); + markdown::code_block(writer, if c { "c" } else { "cpp" }, |writer| { + writer.line(if c { "typedef enum {" } else { "enum class {" }); + writer.indented(|writer| { + for member in &definition.values { + let member_name = if c { + c_enum_member(module, &definition.name, &member.name) + } else { + member.name.clone() + }; + writer.line(format_args!("{member_name},")); + } + }); + if c { + writer.line(format_args!("}} {name};")); + } else { + writer.line("};"); + } + }); + writer.line("| Member | Canonical value | Description |"); + writer.line("| --- | --- | --- |"); + for member in &definition.values { + let member_name = if c { + c_enum_member(module, &definition.name, &member.name) + } else { + member.name.clone() + }; + writer.line(format_args!( + "| `{member_name}` | `{}` | {} |", + escape_table(&member.value), + member + .description + .as_deref() + .map(escape_table) + .unwrap_or_default() + )); + } + writer.blank_line(); +} + fn render_structure(writer: &mut Writer, name: &str, fields: &[Parameter]) { writer.write(format_args!("## `{name}`\n\n")); markdown::code_block(writer, "c", |writer| { @@ -202,7 +261,7 @@ fn render_function_documentation(writer: &mut Writer, function: &CompiledFunctio for parameter in document.parameters { writer.line(format_args!( "| `{}` | in | {} |", - parameter.name, + parameter.name(), parameter_details(parameter) )); } @@ -256,11 +315,16 @@ fn signature(function: &CompiledFunction) -> Result { Ok(format!( "static inline {result} {}({})", function.core.name, - function - .core - .inputs + spec.inputs .iter() - .map(|input| format!("double {input}")) + .map(|input| match input.enum_type() { + Some(definition) => format!( + "{} {}", + c_enum_name(&function.entry.slug, &definition.name), + input.name() + ), + None => format!("double {}", input.name()), + }) .collect::>() .join(", ") )) @@ -274,12 +338,8 @@ fn function_anchor(name: &str) -> String { format!("function-{name}") } -fn parameter_details(parameter: &Parameter) -> String { - format!( - "{} ({})", - escape_table(¶meter.description), - escape_table(¶meter.unit) - ) +fn parameter_details(parameter: &impl docs::ParameterMetadata) -> String { + escape_table(&docs::parameter_details(parameter)) } fn render_admonition(writer: &mut Writer, kind: &str, body: &str) { diff --git a/codegen/src/targets/reference/cpp.rs b/codegen/src/targets/reference/cpp.rs index 8753fd5..53f1b6b 100644 --- a/codegen/src/targets/reference/cpp.rs +++ b/codegen/src/targets/reference/cpp.rs @@ -1,10 +1,11 @@ use std::{collections::BTreeSet, path::PathBuf}; use anyhow::{Result, anyhow}; +use convert_case::{Case, Casing}; use crate::{ documentation::{self as docs}, - model::{CompiledFunction, Output, Parameter}, + model::{CompiledFunction, EnumDefinition, Output, Parameter}, output::GeneratedFile, render::{Writer, markdown}, targets::group_by_source, @@ -138,6 +139,16 @@ fn render_module(writer: &mut Writer, slug: &str, functions: &[&CompiledFunction "[PTF catalog page](../../../ptf-catalog/sources/{slug}.md)\n\n" )); + let mut enums = BTreeSet::new(); + for function in functions { + for input in &spec(function).inputs { + if let Some(definition) = input.enum_type() + && enums.insert(definition.name.clone()) + { + render_enum(writer, definition); + } + } + } let mut structures = BTreeSet::new(); for function in functions { if let Output::Struct(_) = &function.core.output { @@ -154,6 +165,34 @@ fn render_module(writer: &mut Writer, slug: &str, functions: &[&CompiledFunction Ok(()) } +fn render_enum(writer: &mut Writer, definition: &EnumDefinition) { + writer.write(format_args!("## `{}`\n\n", definition.name)); + markdown::code_block(writer, "cpp", |writer| { + writer.line(format_args!("enum class {} {{", definition.name)); + writer.indented(|writer| { + for member in &definition.values { + writer.line(format_args!("{},", member.name.to_case(Case::Pascal))); + } + }); + writer.line("};"); + }); + writer.line("| Member | Canonical value | Description |"); + writer.line("| --- | --- | --- |"); + for member in &definition.values { + writer.line(format_args!( + "| `{}` | `{}` | {} |", + member.name.to_case(Case::Pascal), + escape_table(&member.value), + member + .description + .as_deref() + .map(escape_table) + .unwrap_or_default() + )); + } + writer.blank_line(); +} + fn render_structure(writer: &mut Writer, name: &str, fields: &[Parameter]) { writer.write(format_args!("## `{name}`\n\n")); markdown::code_block(writer, "cpp", |writer| { @@ -194,7 +233,7 @@ fn render_function_documentation(writer: &mut Writer, function: &CompiledFunctio for parameter in document.parameters { writer.line(format_args!( "| `{}` | {} |", - parameter.name, + parameter.name(), parameter_details(parameter) )); } @@ -233,6 +272,7 @@ fn render_functions_index(writer: &mut Writer, functions: &[(&str, &CompiledFunc } fn signature(function: &CompiledFunction) -> Result { + let spec = spec(function); let result = match &function.core.output { Output::Scalar => "double".to_owned(), Output::Struct(_) => result_class(function)?.to_owned(), @@ -240,11 +280,12 @@ fn signature(function: &CompiledFunction) -> Result { Ok(format!( "[[nodiscard]]\ninline {result} {}({})", function.core.name, - function - .core - .inputs + spec.inputs .iter() - .map(|input| format!("double {input}")) + .map(|input| match input.enum_type() { + Some(definition) => format!("{} {}", definition.name, input.name()), + None => format!("double {}", input.name()), + }) .collect::>() .join(", ") )) @@ -264,12 +305,8 @@ fn function_anchor(name: &str) -> String { format!("function-{name}") } -fn parameter_details(parameter: &Parameter) -> String { - format!( - "{} ({})", - escape_table(¶meter.description), - escape_table(¶meter.unit) - ) +fn parameter_details(parameter: &impl docs::ParameterMetadata) -> String { + escape_table(&docs::parameter_details(parameter)) } fn render_admonition(writer: &mut Writer, kind: &str, body: &str) { diff --git a/codegen/src/targets/rust.rs b/codegen/src/targets/rust.rs index a559041..292aeba 100644 --- a/codegen/src/targets/rust.rs +++ b/codegen/src/targets/rust.rs @@ -1,16 +1,23 @@ use std::{collections::BTreeSet, path::PathBuf, str::FromStr}; use anyhow::Result; +use convert_case::{Case, Casing}; use proc_macro2::{Ident, Literal, TokenStream, TokenTree}; use quote::{format_ident, quote}; use crate::{ documentation::{self as docs, FunctionDocument, Returns, SourceDocument}, - model::{CompiledFunction, Output}, - semantic::{self, BinaryOp, Expr, MathFunction, Number, Reference, UnaryOp}, + model::{CompiledFunction, CompiledInput, EnumDefinition, Output}, + semantic::{ + self, BinaryOp, Expr, MathFunction, Number, RecordLookup, Reference, ResultBinding, + UnaryOp, VariableValue, + }, }; -use crate::{output::GeneratedFile, targets::group_by_source}; +use crate::{ + output::GeneratedFile, + targets::{group_by_source, record_types}, +}; pub(super) const HEADER: &str = "// @generated by ptfkit-codegen; DO NOT EDIT.\n"; @@ -26,6 +33,30 @@ pub(crate) fn render(functions: &[CompiledFunction]) -> Result 1; + let mut enum_names = BTreeSet::new(); + let enum_definitions = functions + .iter() + .flat_map(|resolved| { + resolved.entry.spec.functions[resolved.function_index] + .inputs + .iter() + .filter_map(|input| input.enum_type()) + }) + .filter(|definition| enum_names.insert(definition.name.as_str())) + .map(enum_tokens) + .collect::>(); + let public_record_names = functions + .iter() + .filter_map(|resolved| { + resolved.entry.spec.functions[resolved.function_index].result_class() + }) + .collect::>(); + let internal_record_definitions = record_types(&functions)? + .into_iter() + .filter(|(name, _)| !public_record_names.contains(name.as_str())) + .map(|(name, fields)| internal_record_tokens(&name, &fields)) + .collect::>(); + let lookup_conversions = lookup_conversion_tokens(&functions); let mut defined_result_classes = BTreeSet::new(); let definitions = functions .into_iter() @@ -39,7 +70,10 @@ pub(crate) fn render(functions: &[CompiledFunction]) -> Result>>()?; Ok(GeneratedFile::new( PathBuf::from(format!("{slug}.rs")), - render_tokens(module_docs, quote!(#(#definitions)*)), + render_tokens( + module_docs, + quote!(#(#enum_definitions)* #(#internal_record_definitions)* #(#lookup_conversions)* #(#definitions)*), + ), )) }) .collect() @@ -60,6 +94,16 @@ fn module_tokens( .iter() .map(|name| format_ident!("{name}")) .collect::>(); + let input_types = specification + .inputs + .iter() + .map(|input| { + input + .enum_type() + .map(|definition| format_ident!("{}", definition.name)) + .map_or_else(|| quote!(f64), |name| quote!(#name)) + }) + .collect::>(); let scalar_output = matches!(function.output, Output::Scalar).then(|| { resolved.entry.spec.functions[resolved.function_index] .outputs @@ -68,19 +112,38 @@ fn module_tokens( .as_str() }); let terminal_output = scalar_output.and_then(|output| { - ir.variables - .last() - .filter(|variable| variable.name == output) + ir.variables.last().filter(|variable| { + variable.name == output && matches!(variable.value, VariableValue::Number(_)) + }) }); + let terminal_record = match ir.result { + ResultBinding::RecordVariable(index) if index + 1 == ir.variables.len() => { + match &ir.variables[index].value { + VariableValue::RecordLookup(lookup) => Some(lookup), + VariableValue::Number(_) => None, + } + } + ResultBinding::Fields | ResultBinding::RecordVariable(_) => None, + }; let variables = ir .variables .iter() - .take(ir.variables.len() - usize::from(terminal_output.is_some())) + .take( + ir.variables.len() + - usize::from(terminal_output.is_some() || terminal_record.is_some()), + ) .map(|variable| { let name = format_ident!("{}", variable.name); - let expression = - expression_tokens(&variable.expression, &inputs, &ir.variables)?.tokens; - Ok(quote!(let #name = #expression;)) + match &variable.value { + VariableValue::Number(expression) => { + let expression = expression_tokens(expression, &inputs, &ir.variables)?.tokens; + Ok(quote!(let #name = #expression;)) + } + VariableValue::RecordLookup(lookup) => { + let expression = record_lookup_tokens(lookup, &inputs, &ir.variables)?; + Ok(quote!(let #name = #expression;)) + } + } }) .collect::>>()?; let OutputTokens { @@ -91,6 +154,7 @@ fn module_tokens( } = output_tokens( resolved, terminal_output, + terminal_record, &inputs, &ir.variables, define_output, @@ -102,7 +166,7 @@ fn module_tokens( #function_docs #[cfg_attr(feature = "inline", inline)] #[must_use] - pub fn #name(#(#inputs: f64),*) -> #return_type { + pub fn #name(#(#inputs: #input_types),*) -> #return_type { #(#variables)* #separates_result #expression @@ -112,6 +176,25 @@ fn module_tokens( }) } +fn enum_tokens(definition: &EnumDefinition) -> TokenStream { + let name = format_ident!("{}", definition.name); + let members = definition.values.iter().map(|member| { + let member_name = format_ident!("{}", member.name.to_case(Case::Pascal)); + let docs = member + .description + .as_ref() + .map(|description| doc_tokens([description.clone()])); + quote!(#docs #member_name) + }); + quote!(#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum #name { #(#members),* }) +} + +fn internal_record_tokens(name: &str, fields: &[String]) -> TokenStream { + let name = format_ident!("{name}"); + let fields = fields.iter().map(|field| format_ident!("{field}")); + quote!(#[derive(Clone, Copy, Debug, PartialEq)] struct #name { #(#fields: f64),* }) +} + struct OutputTokens { definition: TokenStream, return_type: TokenStream, @@ -122,6 +205,7 @@ struct OutputTokens { fn output_tokens( resolved: &CompiledFunction, terminal_output: Option<&semantic::Variable>, + terminal_record: Option<&RecordLookup>, inputs: &[Ident], variables: &[semantic::Variable], define_output: bool, @@ -134,7 +218,10 @@ fn output_tokens( .name; let expression = match terminal_output { Some(variable) => { - expression_tokens(&variable.expression, inputs, variables)?.tokens + let VariableValue::Number(expression) = &variable.value else { + unreachable!("scalar terminal output is numeric") + }; + expression_tokens(expression, inputs, variables)?.tokens } None => { let name = format_ident!("{name}"); @@ -165,7 +252,23 @@ fn output_tokens( let docs = doc_tokens([docs::parameter_details(parameter)]); quote!(#docs pub #field: f64) }); - let values = fields.iter().map(|field| format_ident!("{field}")); + let expression = match resolved.ir.result { + ResultBinding::RecordVariable(_) if terminal_record.is_some() => { + record_lookup_tokens( + terminal_record.expect("checked terminal record"), + inputs, + variables, + )? + } + ResultBinding::RecordVariable(index) => { + let variable = format_ident!("{}", resolved.ir.variables[index].name); + quote!(#variable) + } + ResultBinding::Fields => { + let values = fields.iter().map(|field| format_ident!("{field}")); + quote!(#result { #(#values),* }) + } + }; Ok(OutputTokens { definition: if define_output { let docs = @@ -175,13 +278,70 @@ fn output_tokens( TokenStream::new() }, return_type: quote!(#result), - expression: quote!(#result { #(#values),* }), + expression, separates_result: quote!(), }) } } } +fn record_lookup_tokens( + lookup: &RecordLookup, + inputs: &[Ident], + variables: &[semantic::Variable], +) -> Result { + let key = reference_ident(lookup.key, inputs, variables); + Ok(quote!(#key.into())) +} + +fn lookup_conversion_tokens(functions: &[&CompiledFunction]) -> Vec { + let mut names = BTreeSet::new(); + functions + .iter() + .flat_map(|function| &function.ir.variables) + .filter_map(|variable| match &variable.value { + VariableValue::RecordLookup(lookup) => Some(lookup), + VariableValue::Number(_) => None, + }) + .filter(|lookup| names.insert((lookup.enum_name.clone(), lookup.output.name.clone()))) + .map(|lookup| { + let enum_name = format_ident!("{}", lookup.enum_name); + let output = format_ident!("{}", lookup.output.name); + let fields = lookup + .output + .fields + .iter() + .map(|field| format_ident!("{field}")) + .collect::>(); + let arms = lookup.cases.iter().map(|case| { + let member = format_ident!("{}", case.member.to_case(Case::Pascal)); + let values = case.values.iter().map(rust_float_literal); + quote!(#enum_name::#member => Self { #(#fields: #values),* }) + }); + quote! { + impl From<#enum_name> for #output { + fn from(value: #enum_name) -> Self { + match value { + #(#arms),* + } + } + } + } + }) + .collect() +} + +fn reference_ident( + reference: Reference, + inputs: &[Ident], + variables: &[semantic::Variable], +) -> Ident { + match reference { + Reference::Input(index) => inputs[index].clone(), + Reference::Variable(index) => format_ident!("{}", variables[index].name), + } +} + fn module_doc_tokens(document: SourceDocument<'_>) -> TokenStream { let mut lines = vec![ document.summary.into(), @@ -416,6 +576,11 @@ fn expression_tokens( precedence: Precedence::Primary, }) } + Expr::Field { record, field } => { + let record = reference_ident(*record, inputs, variables); + let field = format_ident!("{field}"); + Ok(primary_tokens(quote!(#record.#field))) + } Expr::Unary { op, operand } => { let operand = expression_tokens(operand, inputs, variables)?; Ok(match op { @@ -536,7 +701,21 @@ fn golden_test_tokens(resolved: &CompiledFunction, unique_test_module: bool) -> let values = case .inputs .iter() - .map(|value| Literal::f64_suffixed(*value)) + .map(|value| match value { + CompiledInput::Number(value) => { + let value = Literal::f64_suffixed(*value); + quote!(#value) + } + CompiledInput::Enum { + enum_name, + member_name, + .. + } => { + let enum_name = format_ident!("{enum_name}"); + let member_name = format_ident!("{}", member_name.to_case(Case::Pascal)); + quote!(#enum_name::#member_name) + } + }) .collect::>(); let expected = case .expected @@ -698,6 +877,27 @@ mod tests { assert_eq!(literal("2."), "2.0f64"); } + #[test] + fn renders_record_field_access() { + let expression = Expr::Field { + record: Reference::Variable(0), + field: "b".into(), + }; + let variables = [semantic::Variable { + name: "parameters".into(), + value: VariableValue::Number(Expr::Number(Number { + value: 0.0, + lexeme: "0.0".into(), + })), + }]; + let rendered = expression_tokens(&expression, &[], &variables) + .unwrap() + .tokens; + + assert!(syn::parse2::(rendered.clone()).is_ok()); + assert_eq!(rendered.to_string(), "parameters . b"); + } + #[test] fn renders_outer_and_inner_rustdoc_attributes() { let module_docs = inner_doc_tokens(["Source summary.".into(), "# Reference".into()]); diff --git a/codegen/src/validate.rs b/codegen/src/validate.rs index 47bee07..cbfb6a0 100644 --- a/codegen/src/validate.rs +++ b/codegen/src/validate.rs @@ -26,7 +26,8 @@ pub(crate) fn specifications(entries: &[Entry]) -> Vec { "functions[].public_api", &mut errors, ); - duplicate_names(entry, function, &function.inputs, "inputs", &mut errors); + duplicate_input_names(entry, function, &mut errors); + validate_enums(entry, function, &mut errors); duplicate_names( entry, function, @@ -63,6 +64,69 @@ pub(crate) fn specifications(entries: &[Entry]) -> Vec { errors } +fn duplicate_input_names(entry: &Entry, function: &Function, errors: &mut Vec) { + let mut seen = BTreeSet::new(); + for value in &function.inputs { + if !seen.insert(value.name()) { + errors.push(diag( + entry, + "inputs", + Some(&function.name), + &format!("duplicate name `{}`", value.name()), + )); + } + } +} + +fn validate_enums(entry: &Entry, function: &Function, errors: &mut Vec) { + let mut validated = BTreeSet::new(); + for input in &function.inputs { + let Some(enum_type) = input.enum_type() else { + continue; + }; + if !validated.insert(&enum_type.name) { + continue; + } + let mut names = BTreeSet::new(); + let mut values = BTreeSet::new(); + for member in &enum_type.values { + if !names.insert(&member.name) { + errors.push(diag( + entry, + "$defs", + Some(&function.name), + &format!( + "enum `{}` contains duplicate member name `{}`", + enum_type.name, member.name + ), + )); + } + if !values.insert(&member.value) { + errors.push(diag( + entry, + "$defs", + Some(&function.name), + &format!( + "enum `{}` contains duplicate canonical value `{}`", + enum_type.name, member.value + ), + )); + } + } + if enum_type.values.len() > u32::MAX as usize { + errors.push(diag( + entry, + "$defs", + Some(&function.name), + &format!( + "enum `{}` exceeds the target ordinal capacity", + enum_type.name + ), + )); + } + } +} + fn duplicate( map: &mut BTreeMap, key: &K, diff --git a/docs/src/ptf-catalog/index.md b/docs/src/ptf-catalog/index.md index f024af7..c74e3ee 100644 --- a/docs/src/ptf-catalog/index.md +++ b/docs/src/ptf-catalog/index.md @@ -63,19 +63,26 @@ The `status` communicates how far the function has progressed: The catalog may therefore document functions that are not yet callable. Users should check both the function status and the API reference for their target. -## Quantities, units, and domains +## Inputs, quantities, units, and domains -Every input and output identifies the scientific quantity through a name, +Quantitative inputs and outputs identify a scientific quantity through a name, symbol, unit, domain, and description. These values must preserve the source's definitions and conversions. A domain records the published calibration or mathematical range; it does not imply that every target performs runtime range validation. +Categorical inputs reference a self-contained enum definition and bind it to a +function argument name. The enum owns its type description and admissible +values, while the binding may optionally describe the argument's role in that +function. Units, numeric domains, and scientific symbols do not apply to enum +inputs. The binding name belongs to the function, so one enum type can be used +under different argument names. + Outputs are either scalar values or records with ordered fields. Record names are stable public type names, while field order is part of the cross-target -result contract. Reusable parameters and record shapes may be declared once in -`$defs` and referenced by multiple functions. The `$defs` key is the canonical -name of a reusable model or record. +result contract. Reusable parameter declarations, enum types, and record shapes +may be declared once in `$defs` and referenced by multiple functions. The +`$defs` key is the canonical name of a reusable declaration, type, or record. ## Scientific evidence and numerical expectations @@ -96,9 +103,23 @@ can prevent a function from advancing beyond `draft` or `blocked`. ## Implementation data Functions marked `ready-for-implementation` or `implemented` include an -`implementation`. It expresses the ordered intermediate and output variables -used to reproduce the published PTF. Scalar outputs resolve to one value; -record outputs resolve their declared fields by name. +`implementation`. Implementations express ordered variables used to reproduce +the published PTF. A variable can be populated by a formula or by a typed lookup. +Enums, records, and lookups are independent reusable definitions: a lookup maps +an enum member to a record, and later formulas can access fields of that record. +Enum definitions give each categorical member a stable schema `name`, its exact +canonical textual `value`, and optional documentation-only `description`. +Lookup rows reference the member `name`; they do not define public numeric codes +or match canonical strings at runtime. Targets may encode members with private +ordinals as an implementation detail. Scalar outputs resolve to one value, +while record outputs resolve their declared fields by name or return a compatible +record-valued variable directly. + +Python exposes scalar categories as ordinary `Enum` members. Reusable arrays +are constructed once with `EnumType.array(...)` and represented by a typed +`EnumArray[EnumType]`; generated wrappers pass its private `uint32` NumPy array +to the native ufunc without re-encoding it on each call. Strings, integers, and +arbitrary arrays are not accepted as enum inputs. The YAML is the canonical target-independent representation. Language-specific details, generated file ownership, and the commands used to validate and diff --git a/docs/src/ptf-catalog/sources/ahuja1984.md b/docs/src/ptf-catalog/sources/ahuja1984.md index 2f51825..f417419 100644 --- a/docs/src/ptf-catalog/sources/ahuja1984.md +++ b/docs/src/ptf-catalog/sources/ahuja1984.md @@ -31,12 +31,12 @@ Estimate saturated hydraulic conductivity from total porosity and water content #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `total_porosity` | cm^3/cm^3 | 0 <= value <= 1 | Total porosity; in the experiments it was taken as saturated volumetric water content. | -| `theta_33` | cm^3/cm^3 | 0 <= value <= 1 | Volumetric soil water content at -33 kPa pressure head. | -| `coefficient_b` | cm/h | value > 0 | Soil-specific empirical coefficient in the generalized Kozeny-Carman relation. | -| `exponent_n` | 1 | value > 0 | Empirical exponent; the paper evaluates values of 4 and 5 for deriving conductivity scaling-factor distributions. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `total_porosity` | `number` | cm^3/cm^3 | 0 <= value <= 1 | Total porosity; in the experiments it was taken as saturated volumetric water content. | +| `theta_33` | `number` | cm^3/cm^3 | 0 <= value <= 1 | Volumetric soil water content at -33 kPa pressure head. | +| `coefficient_b` | `number` | cm/h | value > 0 | Soil-specific empirical coefficient in the generalized Kozeny-Carman relation. | +| `exponent_n` | `number` | 1 | value > 0 | Empirical exponent; the paper evaluates values of 4 and 5 for deriving conductivity scaling-factor distributions. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/aimrun2009.md b/docs/src/ptf-catalog/sources/aimrun2009.md index 1ddbc31..805a06f 100644 --- a/docs/src/ptf-catalog/sources/aimrun2009.md +++ b/docs/src/ptf-catalog/sources/aimrun2009.md @@ -33,12 +33,12 @@ Estimate saturated hydraulic conductivity for lowland paddy soils. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | value > 0 | Clay content, <2 um. | -| `bulk_density` | g/cm^3 | value > 0 | Dry bulk density. | -| `organic_matter` | % | value > 0 | Organic matter content. | -| `gmd` | mm | value > 0 | Geometric mean diameter of texture. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | value > 0 | Clay content, <2 um. | +| `bulk_density` | `number` | g/cm^3 | value > 0 | Dry bulk density. | +| `organic_matter` | `number` | % | value > 0 | Organic matter content. | +| `gmd` | `number` | mm | value > 0 | Geometric mean diameter of texture. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/beniaich2023.md b/docs/src/ptf-catalog/sources/beniaich2023.md index 4d63d29..a196547 100644 --- a/docs/src/ptf-catalog/sources/beniaich2023.md +++ b/docs/src/ptf-catalog/sources/beniaich2023.md @@ -33,9 +33,9 @@ Estimate three gravimetric water contents from clay. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | 0 <= value <= 100 | Clay content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by mass. | #### Outputs @@ -65,9 +65,9 @@ Estimate three gravimetric water contents from silt. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `silt` | % | 0 <= value <= 100 | Silt content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `silt` | `number` | % | 0 <= value <= 100 | Silt content by mass. | #### Outputs @@ -97,9 +97,9 @@ Estimate three gravimetric water contents from sand. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by mass. | #### Outputs @@ -129,10 +129,10 @@ Estimate three gravimetric water contents from clay plus silt. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | 0 <= value <= 100 | Clay content by mass. | -| `silt` | % | 0 <= value <= 100 | Silt content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by mass. | +| `silt` | `number` | % | 0 <= value <= 100 | Silt content by mass. | #### Outputs @@ -162,10 +162,10 @@ Estimate three gravimetric water contents from the clay-to-silt ratio. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | 0 <= value <= 100 | Clay content by mass. | -| `silt` | % | 0 < value <= 100 | Silt content by mass and denominator of the clay-to-silt ratio. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by mass. | +| `silt` | `number` | % | 0 < value <= 100 | Silt content by mass and denominator of the clay-to-silt ratio. | #### Outputs @@ -195,9 +195,9 @@ Estimate three gravimetric water contents from soil organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `soil_organic_matter` | % | value >= 0 | Soil organic matter content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `soil_organic_matter` | `number` | % | value >= 0 | Soil organic matter content by mass. | #### Outputs @@ -227,11 +227,11 @@ Estimate three gravimetric water contents from silt, sand, and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `silt` | % | 0 <= value <= 100 | Silt content by mass. | -| `sand` | % | 0 <= value <= 100 | Sand content by mass. | -| `soil_organic_matter` | % | value >= 0 | Soil organic matter content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `silt` | `number` | % | 0 <= value <= 100 | Silt content by mass. | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by mass. | +| `soil_organic_matter` | `number` | % | value >= 0 | Soil organic matter content by mass. | #### Outputs @@ -261,10 +261,10 @@ Estimate three gravimetric water contents from sand and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by mass. | -| `soil_organic_matter` | % | value >= 0 | Soil organic matter content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by mass. | +| `soil_organic_matter` | `number` | % | value >= 0 | Soil organic matter content by mass. | #### Outputs @@ -294,10 +294,10 @@ Estimate three gravimetric water contents from silt and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `silt` | % | 0 <= value <= 100 | Silt content by mass. | -| `soil_organic_matter` | % | value >= 0 | Soil organic matter content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `silt` | `number` | % | 0 <= value <= 100 | Silt content by mass. | +| `soil_organic_matter` | `number` | % | value >= 0 | Soil organic matter content by mass. | #### Outputs @@ -327,10 +327,10 @@ Estimate three gravimetric water contents from clay and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | 0 <= value <= 100 | Clay content by mass. | -| `soil_organic_matter` | % | value >= 0 | Soil organic matter content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by mass. | +| `soil_organic_matter` | `number` | % | value >= 0 | Soil organic matter content by mass. | #### Outputs @@ -360,11 +360,11 @@ Estimate three gravimetric water contents from clay, silt, and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | 0 <= value <= 100 | Clay content by mass. | -| `silt` | % | 0 <= value <= 100 | Silt content by mass. | -| `soil_organic_matter` | % | value >= 0 | Soil organic matter content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by mass. | +| `silt` | `number` | % | 0 <= value <= 100 | Silt content by mass. | +| `soil_organic_matter` | `number` | % | value >= 0 | Soil organic matter content by mass. | #### Outputs @@ -394,12 +394,12 @@ Estimate three gravimetric water contents with the fitted regression trees. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | 0 <= value <= 100 | Clay content by mass. | -| `silt` | % | 0 <= value <= 100 | Silt content by mass. | -| `sand` | % | 0 <= value <= 100 | Sand content by mass. | -| `soil_organic_matter` | % | value >= 0 | Soil organic matter content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by mass. | +| `silt` | `number` | % | 0 <= value <= 100 | Silt content by mass. | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by mass. | +| `soil_organic_matter` | `number` | % | value >= 0 | Soil organic matter content by mass. | #### Outputs @@ -429,12 +429,12 @@ Estimate three gravimetric water contents with the fitted Cubist models. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | 0 <= value <= 100 | Clay content by mass. | -| `silt` | % | 0 <= value <= 100 | Silt content by mass. | -| `sand` | % | 0 <= value <= 100 | Sand content by mass. | -| `soil_organic_matter` | % | value >= 0 | Soil organic matter content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by mass. | +| `silt` | `number` | % | 0 <= value <= 100 | Silt content by mass. | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by mass. | +| `soil_organic_matter` | `number` | % | value >= 0 | Soil organic matter content by mass. | #### Outputs @@ -472,12 +472,12 @@ Estimate three gravimetric water contents with the fitted random forests. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | 0 <= value <= 100 | Clay content by mass. | -| `silt` | % | 0 <= value <= 100 | Silt content by mass. | -| `sand` | % | 0 <= value <= 100 | Sand content by mass. | -| `soil_organic_matter` | % | value >= 0 | Soil organic matter content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by mass. | +| `silt` | `number` | % | 0 <= value <= 100 | Silt content by mass. | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by mass. | +| `soil_organic_matter` | `number` | % | value >= 0 | Soil organic matter content by mass. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/chakraborty2011.md b/docs/src/ptf-catalog/sources/chakraborty2011.md index 0c4a663..3ea0abe 100644 --- a/docs/src/ptf-catalog/sources/chakraborty2011.md +++ b/docs/src/ptf-catalog/sources/chakraborty2011.md @@ -31,10 +31,10 @@ Estimate four gravimetric water contents from clay and silt. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | — | Clay content by mass. | -| `silt` | % | — | Silt content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | — | Clay content by mass. | +| `silt` | `number` | % | — | Silt content by mass. | #### Outputs @@ -69,10 +69,10 @@ Estimate four gravimetric water contents from sand and bulk density. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | — | Sand content by mass. | -| `bulk_density` | Mg/m^3 | 1.17 <= value <= 1.98 | Dry soil bulk density. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | — | Sand content by mass. | +| `bulk_density` | `number` | Mg/m^3 | 1.17 <= value <= 1.98 | Dry soil bulk density. | #### Outputs @@ -107,11 +107,11 @@ Estimate four gravimetric water contents from clay, silt, and bulk density. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | — | Clay content by mass. | -| `silt` | % | — | Silt content by mass. | -| `bulk_density` | Mg/m^3 | 1.17 <= value <= 1.98 | Dry soil bulk density. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | — | Clay content by mass. | +| `silt` | `number` | % | — | Silt content by mass. | +| `bulk_density` | `number` | Mg/m^3 | 1.17 <= value <= 1.98 | Dry soil bulk density. | #### Outputs @@ -146,11 +146,11 @@ Estimate four gravimetric water contents from clay, silt, and sand. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | — | Clay content by mass. | -| `silt` | % | — | Silt content by mass. | -| `sand` | % | — | Sand content by mass. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | — | Clay content by mass. | +| `silt` | `number` | % | — | Silt content by mass. | +| `sand` | `number` | % | — | Sand content by mass. | #### Outputs @@ -185,12 +185,12 @@ Estimate four gravimetric water contents from clay, silt, sand, and bulk density #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | — | Clay content by mass. | -| `silt` | % | — | Silt content by mass. | -| `sand` | % | — | Sand content by mass. | -| `bulk_density` | Mg/m^3 | 1.17 <= value <= 1.98 | Dry soil bulk density. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | — | Clay content by mass. | +| `silt` | `number` | % | — | Silt content by mass. | +| `sand` | `number` | % | — | Sand content by mass. | +| `bulk_density` | `number` | Mg/m^3 | 1.17 <= value <= 1.98 | Dry soil bulk density. | #### Outputs @@ -225,13 +225,13 @@ Estimate four gravimetric water contents from texture, organic carbon, and bulk #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | — | Clay content by mass. | -| `silt` | % | — | Silt content by mass. | -| `sand` | % | — | Sand content by mass. | -| `organic_carbon` | % | 0.02 <= value <= 1.18 | Soil organic carbon content by mass. | -| `bulk_density` | Mg/m^3 | 1.17 <= value <= 1.98 | Dry soil bulk density. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | — | Clay content by mass. | +| `silt` | `number` | % | — | Silt content by mass. | +| `sand` | `number` | % | — | Sand content by mass. | +| `organic_carbon` | `number` | % | 0.02 <= value <= 1.18 | Soil organic carbon content by mass. | +| `bulk_density` | `number` | Mg/m^3 | 1.17 <= value <= 1.98 | Dry soil bulk density. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/cosby1984.md b/docs/src/ptf-catalog/sources/cosby1984.md index 35426cd..03fed6c 100644 --- a/docs/src/ptf-catalog/sources/cosby1984.md +++ b/docs/src/ptf-catalog/sources/cosby1984.md @@ -31,11 +31,11 @@ Estimate Cosby et al. (1984) univariate hydraulic parameter statistics from soil #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content. | -| `silt` | % | 0 <= value <= 100 | Silt content. | -| `clay` | % | 0 <= value <= 100 | Clay content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content. | +| `silt` | `number` | % | 0 <= value <= 100 | Silt content. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/dharumarajan2019.md b/docs/src/ptf-catalog/sources/dharumarajan2019.md index 3906029..34aa636 100644 --- a/docs/src/ptf-catalog/sources/dharumarajan2019.md +++ b/docs/src/ptf-catalog/sources/dharumarajan2019.md @@ -33,11 +33,11 @@ Estimate field capacity and wilting point for Northern Karnataka soils. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | Northern study range: 1.2 <= value <= 80.8 | Clay content. | -| `sand` | % | Northern study range: 2.7 <= value <= 94.0 | Sand content. | -| `cation_exchange_capacity` | C mol p+/kg | Northern study range: 1.7 <= value <= 80.9 | Cation exchange capacity. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | Northern study range: 1.2 <= value <= 80.8 | Clay content. | +| `sand` | `number` | % | Northern study range: 2.7 <= value <= 94.0 | Sand content. | +| `cation_exchange_capacity` | `number` | C mol p+/kg | Northern study range: 1.7 <= value <= 80.9 | Cation exchange capacity. | #### Outputs @@ -74,9 +74,9 @@ Estimate Northern Karnataka field capacity and wilting point from clay. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | Northern study range: 1.2 <= value <= 80.8 | Clay content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | Northern study range: 1.2 <= value <= 80.8 | Clay content. | #### Outputs @@ -113,11 +113,11 @@ Estimate field capacity and wilting point for Southern Karnataka soils. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | Southern study range: 5.8 <= value <= 67.8 | Clay content. | -| `sand` | % | Southern study range: 4.4 <= value <= 92.3 | Sand content. | -| `cation_exchange_capacity` | C mol p+/kg | Southern study range: 1.2 <= value <= 52.6 | Cation exchange capacity. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | Southern study range: 5.8 <= value <= 67.8 | Clay content. | +| `sand` | `number` | % | Southern study range: 4.4 <= value <= 92.3 | Sand content. | +| `cation_exchange_capacity` | `number` | C mol p+/kg | Southern study range: 1.2 <= value <= 52.6 | Cation exchange capacity. | #### Outputs @@ -150,9 +150,9 @@ Estimate Southern Karnataka field capacity and wilting point from clay. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | Southern study range: 5.8 <= value <= 67.8 | Clay content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | Southern study range: 5.8 <= value <= 67.8 | Clay content. | #### Outputs @@ -185,11 +185,11 @@ Estimate infiltration rate for Karnataka soils from texture fractions. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | — | Sand content in the Karnataka infiltration dataset. | -| `silt` | % | — | Silt content in the Karnataka infiltration dataset. | -| `clay` | % | — | Clay content in the Karnataka infiltration dataset. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | — | Sand content in the Karnataka infiltration dataset. | +| `silt` | `number` | % | — | Silt content in the Karnataka infiltration dataset. | +| `clay` | `number` | % | — | Clay content in the Karnataka infiltration dataset. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/ferrerjulia2004.md b/docs/src/ptf-catalog/sources/ferrerjulia2004.md index 0415f60..eb05c1b 100644 --- a/docs/src/ptf-catalog/sources/ferrerjulia2004.md +++ b/docs/src/ptf-catalog/sources/ferrerjulia2004.md @@ -33,10 +33,10 @@ Evaluate the Campbell and Shiozawa saturated-conductivity PTF. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | #### Outputs @@ -60,10 +60,10 @@ Evaluate the Saxton et al. saturated-conductivity PTF. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 < value <= 100 | Clay content by percentage; strictly positive for the base-10 logarithm. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 < value <= 100 | Clay content by percentage; strictly positive for the base-10 logarithm. | #### Outputs @@ -91,9 +91,9 @@ Evaluate the Dane and Puckett saturated-conductivity PTF. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | #### Outputs @@ -117,9 +117,9 @@ Evaluate the Puckett et al. saturated-conductivity PTF. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | #### Outputs @@ -143,10 +143,10 @@ Evaluate the Cosby et al. saturated-conductivity PTF. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | #### Outputs @@ -170,9 +170,9 @@ Estimate saturated conductivity for Humic Acrisol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -196,11 +196,11 @@ Estimate saturated conductivity for Humic Acrisol from texture and organic matte #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -224,9 +224,9 @@ Estimate saturated conductivity for Calcic Cambisol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -250,11 +250,11 @@ Estimate saturated conductivity for Calcic Cambisol from texture and organic mat #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -278,9 +278,9 @@ Estimate saturated conductivity for Dystric Cambisol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -304,11 +304,11 @@ Estimate saturated conductivity for Dystric Cambisol from texture and organic ma #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -332,9 +332,9 @@ Estimate saturated conductivity for Eutric Cambisol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -358,11 +358,11 @@ Estimate saturated conductivity for Eutric Cambisol from texture and organic mat #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -386,9 +386,9 @@ Estimate saturated conductivity for Gleyic Cambisol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -412,11 +412,11 @@ Estimate saturated conductivity for Gleyic Cambisol from texture and organic mat #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -440,9 +440,9 @@ Estimate saturated conductivity for Humic Cambisol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -466,11 +466,11 @@ Estimate saturated conductivity for Humic Cambisol from texture and organic matt #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -494,9 +494,9 @@ Estimate saturated conductivity for Calcaric Fluvisol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -520,11 +520,11 @@ Estimate saturated conductivity for Calcaric Fluvisol from texture and organic m #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -548,9 +548,9 @@ Estimate saturated conductivity for Calcic Luvisol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -578,11 +578,11 @@ Estimate saturated conductivity for Calcic Luvisol from texture and organic matt #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -610,9 +610,9 @@ Estimate saturated conductivity for Chromic Luvisol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -636,11 +636,11 @@ Estimate saturated conductivity for Chromic Luvisol from texture and organic mat #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -664,9 +664,9 @@ Estimate saturated conductivity for Gleyic Luvisol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -690,11 +690,11 @@ Estimate saturated conductivity for Gleyic Luvisol from texture and organic matt #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -718,9 +718,9 @@ Estimate saturated conductivity for Orthic Luvisol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -744,11 +744,11 @@ Estimate saturated conductivity for Orthic Luvisol from texture and organic matt #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -772,9 +772,9 @@ Estimate saturated conductivity for Ranker from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -798,11 +798,11 @@ Estimate saturated conductivity for Ranker from texture and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -826,9 +826,9 @@ Estimate saturated conductivity for Calcaric Regosol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -852,11 +852,11 @@ Estimate saturated conductivity for Calcaric Regosol from texture and organic ma #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -880,9 +880,9 @@ Estimate saturated conductivity for Dystric Regosol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -906,11 +906,11 @@ Estimate saturated conductivity for Dystric Regosol from texture and organic mat #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -934,9 +934,9 @@ Estimate saturated conductivity for Eutric Regosol from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -960,11 +960,11 @@ Estimate saturated conductivity for Eutric Regosol from texture and organic matt #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -988,9 +988,9 @@ Estimate saturated conductivity for Rendzina from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -1018,11 +1018,11 @@ Estimate saturated conductivity for Rendzina from texture and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -1050,9 +1050,9 @@ Estimate saturated conductivity for Gleyic Solonchak from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -1080,11 +1080,11 @@ Estimate saturated conductivity for Gleyic Solonchak from texture and organic ma #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs @@ -1120,9 +1120,9 @@ Estimate saturated conductivity for Spanish soils from sand content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | #### Outputs @@ -1150,11 +1150,11 @@ Estimate saturated conductivity for Spanish soils from texture and organic matte #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content by percentage. | -| `clay` | % | 0 <= value <= 100 | Clay content by percentage. | -| `organic_matter` | % | 0 <= value <= 100 | Organic matter content by percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content by percentage. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content by percentage. | +| `organic_matter` | `number` | % | 0 <= value <= 100 | Organic matter content by percentage. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/hodnett2002.md b/docs/src/ptf-catalog/sources/hodnett2002.md index 29069af..cdaae6a 100644 --- a/docs/src/ptf-catalog/sources/hodnett2002.md +++ b/docs/src/ptf-catalog/sources/hodnett2002.md @@ -33,15 +33,15 @@ Estimate four van Genuchten water-retention parameters for tropical soils. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0.5 <= value <= 99.0 | Sand content in the 0.05-2 mm USDA particle-size fraction, expressed as a percentage. | -| `silt` | % | 0.4 <= value <= 77.4 | Silt content in the 0.002-0.05 mm USDA particle-size fraction, expressed as a percentage. | -| `clay` | % | 0.0 <= value <= 95.4 | Clay content in the less-than-0.002 mm USDA particle-size fraction, expressed as a percentage. | -| `organic_carbon` | % | 0.0 <= value <= 30.8 | Soil organic carbon content expressed as a percentage. | -| `bulk_density` | Mg/m^3 | 0.28 <= value <= 1.88 | Soil bulk density. | -| `cation_exchange_capacity` | cmol/kg | 0.0 <= value <= 93.7 | Soil cation exchange capacity. | -| `ph` | dimensionless | 3.60 <= value <= 9.60 | Soil pH. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0.5 <= value <= 99.0 | Sand content in the 0.05-2 mm USDA particle-size fraction, expressed as a percentage. | +| `silt` | `number` | % | 0.4 <= value <= 77.4 | Silt content in the 0.002-0.05 mm USDA particle-size fraction, expressed as a percentage. | +| `clay` | `number` | % | 0.0 <= value <= 95.4 | Clay content in the less-than-0.002 mm USDA particle-size fraction, expressed as a percentage. | +| `organic_carbon` | `number` | % | 0.0 <= value <= 30.8 | Soil organic carbon content expressed as a percentage. | +| `bulk_density` | `number` | Mg/m^3 | 0.28 <= value <= 1.88 | Soil bulk density. | +| `cation_exchange_capacity` | `number` | cmol/kg | 0.0 <= value <= 93.7 | Soil cation exchange capacity. | +| `ph` | `number` | dimensionless | 3.60 <= value <= 9.60 | Soil pH. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/jabro1992.md b/docs/src/ptf-catalog/sources/jabro1992.md index 3ac50c6..352995b 100644 --- a/docs/src/ptf-catalog/sources/jabro1992.md +++ b/docs/src/ptf-catalog/sources/jabro1992.md @@ -33,11 +33,11 @@ Estimate saturated hydraulic conductivity from silt, clay, and bulk density. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `silt` | % | value > 0 | Silt content, 0.002-0.05 mm. | -| `clay` | % | value > 0 | Clay content, <0.002 mm. | -| `bulk_density` | g/cm^3 | value > 0 | Bulk density. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `silt` | `number` | % | value > 0 | Silt content, 0.002-0.05 mm. | +| `clay` | `number` | % | value > 0 | Clay content, <0.002 mm. | +| `bulk_density` | `number` | g/cm^3 | value > 0 | Bulk density. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/li2007.md b/docs/src/ptf-catalog/sources/li2007.md index 40f8226..9c0ad50 100644 --- a/docs/src/ptf-catalog/sources/li2007.md +++ b/docs/src/ptf-catalog/sources/li2007.md @@ -33,13 +33,13 @@ Estimate van Genuchten parameters and saturated hydraulic conductivity for Fengq #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | value > 0 | Sand content, 0.02-2 mm. | -| `silt` | % | value > 0 | Silt content, 0.02-0.002 mm. | -| `clay` | % | value > 0 | Clay content, <0.002 mm. | -| `bulk_density` | g/cm^3 | value > 0 | Bulk density. | -| `soil_organic_matter` | % | value > 0 | Soil organic matter. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | value > 0 | Sand content, 0.02-2 mm. | +| `silt` | `number` | % | value > 0 | Silt content, 0.02-0.002 mm. | +| `clay` | `number` | % | value > 0 | Clay content, <0.002 mm. | +| `bulk_density` | `number` | g/cm^3 | value > 0 | Bulk density. | +| `soil_organic_matter` | `number` | % | value > 0 | Soil organic matter. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/mayr1999.md b/docs/src/ptf-catalog/sources/mayr1999.md index b9d64a9..6a13dc1 100644 --- a/docs/src/ptf-catalog/sources/mayr1999.md +++ b/docs/src/ptf-catalog/sources/mayr1999.md @@ -33,13 +33,13 @@ Estimate modified Brooks-Corey a, b, and saturated water content from texture, b #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0 <= value <= 100 | Sand content for particles 0.063-2.0 mm. | -| `silt` | % | 0 <= value <= 100 | Silt content for particles 0.002-0.063 mm. | -| `clay` | % | 0 <= value <= 100 | Clay content for particles 0-0.002 mm. | -| `bulk_density` | g/cm^3 | value >= 0.9 | Dry bulk density; the paper excludes application below 0.9 g/cm^3. | -| `organic_carbon` | % | 0 <= value <= 5 | Organic carbon content; the paper excludes application above 5%. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0 <= value <= 100 | Sand content for particles 0.063-2.0 mm. | +| `silt` | `number` | % | 0 <= value <= 100 | Silt content for particles 0.002-0.063 mm. | +| `clay` | `number` | % | 0 <= value <= 100 | Clay content for particles 0-0.002 mm. | +| `bulk_density` | `number` | g/cm^3 | value >= 0.9 | Dry bulk density; the paper excludes application below 0.9 g/cm^3. | +| `organic_carbon` | `number` | % | 0 <= value <= 5 | Organic carbon content; the paper excludes application above 5%. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/oosterveld1980.md b/docs/src/ptf-catalog/sources/oosterveld1980.md index e597f16..0bac44d 100644 --- a/docs/src/ptf-catalog/sources/oosterveld1980.md +++ b/docs/src/ptf-catalog/sources/oosterveld1980.md @@ -31,9 +31,9 @@ Estimate field-capacity tension from clay content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % mass | 2 <= value <= 72 | Clay content in percent by weight; the field-capacity tension experiment covered 2% to 72% clay. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % mass | 2 <= value <= 72 | Clay content in percent by weight; the field-capacity tension experiment covered 2% to 72% clay. | #### Outputs @@ -57,12 +57,12 @@ Estimate gravimetric soil-moisture content from texture, depth, and tension. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % mass | — | Clay content in percent by weight. | -| `sand` | % mass | — | Sand content in percent by weight. | -| `mean_depth` | cm | 8 <= value <= 180 | Mean depth of the soil sample. | -| `tension` | kPa | 10 <= value <= 1500 | Soil-moisture tension. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % mass | — | Clay content in percent by weight. | +| `sand` | `number` | % mass | — | Sand content in percent by weight. | +| `mean_depth` | `number` | cm | 8 <= value <= 180 | Mean depth of the soil sample. | +| `tension` | `number` | kPa | 10 <= value <= 1500 | Soil-moisture tension. | #### Outputs @@ -90,11 +90,11 @@ Estimate gravimetric soil-moisture content at field capacity. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % mass | 2 <= value <= 72 | Clay content in percent by weight; the field-capacity tension experiment covered 2% to 72% clay. | -| `sand` | % mass | — | Sand content in percent by weight. | -| `mean_depth` | cm | 8 <= value <= 180 | Mean depth of the soil sample. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % mass | 2 <= value <= 72 | Clay content in percent by weight; the field-capacity tension experiment covered 2% to 72% clay. | +| `sand` | `number` | % mass | — | Sand content in percent by weight. | +| `mean_depth` | `number` | cm | 8 <= value <= 180 | Mean depth of the soil sample. | #### Outputs @@ -118,11 +118,11 @@ Estimate gravimetric soil-moisture content at the 1500 kPa wilting point. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % mass | — | Clay content in percent by weight. | -| `sand` | % mass | — | Sand content in percent by weight. | -| `mean_depth` | cm | 8 <= value <= 180 | Mean depth of the soil sample. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % mass | — | Clay content in percent by weight. | +| `sand` | `number` | % mass | — | Sand content in percent by weight. | +| `mean_depth` | `number` | cm | 8 <= value <= 180 | Mean depth of the soil sample. | #### Outputs @@ -150,11 +150,11 @@ Estimate available gravimetric soil moisture between field capacity and wilting #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % mass | 2 <= value <= 72 | Clay content in percent by weight; the field-capacity tension experiment covered 2% to 72% clay. | -| `sand` | % mass | — | Sand content in percent by weight. | -| `mean_depth` | cm | 8 <= value <= 180 | Mean depth of the soil sample. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % mass | 2 <= value <= 72 | Clay content in percent by weight; the field-capacity tension experiment covered 2% to 72% clay. | +| `sand` | `number` | % mass | — | Sand content in percent by weight. | +| `mean_depth` | `number` | cm | 8 <= value <= 180 | Mean depth of the soil sample. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/pidgeon1972.md b/docs/src/ptf-catalog/sources/pidgeon1972.md index 8652597..356617a 100644 --- a/docs/src/ptf-catalog/sources/pidgeon1972.md +++ b/docs/src/ptf-catalog/sources/pidgeon1972.md @@ -31,11 +31,11 @@ Estimate gravimetric field capacity from silt, clay, and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `silt` | % | — | Silt measured by particle-size method 2. | -| `clay` | % | — | Clay measured by particle-size method 2. | -| `organic_matter` | % | — | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `silt` | `number` | % | — | Silt measured by particle-size method 2. | +| `clay` | `number` | % | — | Clay measured by particle-size method 2. | +| `organic_matter` | `number` | % | — | Organic matter content. | #### Outputs @@ -59,9 +59,9 @@ Estimate gravimetric field capacity from sand. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | — | Sand measured by particle-size method 2. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | — | Sand measured by particle-size method 2. | #### Outputs @@ -81,10 +81,10 @@ Estimate gravimetric field capacity from sand and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | — | Sand measured by particle-size method 2. | -| `organic_matter` | % | — | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | — | Sand measured by particle-size method 2. | +| `organic_matter` | `number` | % | — | Organic matter content. | #### Outputs @@ -104,10 +104,10 @@ Estimate volumetric field capacity from sand and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | — | Sand measured by particle-size method 2. | -| `organic_matter` | % | — | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | — | Sand measured by particle-size method 2. | +| `organic_matter` | `number` | % | — | Organic matter content. | #### Outputs @@ -127,11 +127,11 @@ Estimate permanent wilting point from silt, clay, and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `silt` | % | — | Silt measured by particle-size method 2. | -| `clay` | % | — | Clay measured by particle-size method 2. | -| `organic_matter` | % | — | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `silt` | `number` | % | — | Silt measured by particle-size method 2. | +| `clay` | `number` | % | — | Clay measured by particle-size method 2. | +| `organic_matter` | `number` | % | — | Organic matter content. | #### Outputs @@ -151,9 +151,9 @@ Estimate permanent wilting point from sand. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | — | Sand measured by particle-size method 1. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | — | Sand measured by particle-size method 1. | #### Outputs @@ -173,10 +173,10 @@ Estimate permanent wilting point from sand and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | — | Sand measured by particle-size method 2. | -| `organic_matter` | % | — | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | — | Sand measured by particle-size method 2. | +| `organic_matter` | `number` | % | — | Organic matter content. | #### Outputs @@ -196,10 +196,10 @@ Estimate available water capacity from clay and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | — | Clay measured by particle-size method 1. | -| `organic_matter` | % | — | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | — | Clay measured by particle-size method 1. | +| `organic_matter` | `number` | % | — | Organic matter content. | #### Outputs @@ -219,10 +219,10 @@ Estimate available water capacity from sand and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | — | Sand measured by particle-size method 2. | -| `organic_matter` | % | — | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | — | Sand measured by particle-size method 2. | +| `organic_matter` | `number` | % | — | Organic matter content. | #### Outputs @@ -242,9 +242,9 @@ Estimate available water capacity from coarse sand. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `coarse_sand` | % | — | Coarse sand measured by particle-size method 1. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `coarse_sand` | `number` | % | — | Coarse sand measured by particle-size method 1. | #### Outputs @@ -264,9 +264,9 @@ Estimate available water capacity from fine sand. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `fine_sand` | % | — | Fine sand measured by particle-size method 1. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `fine_sand` | `number` | % | — | Fine sand measured by particle-size method 1. | #### Outputs @@ -286,9 +286,9 @@ Estimate available water capacity from very fine sand. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `very_fine_sand` | % | — | Very fine sand measured by particle-size method 1. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `very_fine_sand` | `number` | % | — | Very fine sand measured by particle-size method 1. | #### Outputs @@ -308,11 +308,11 @@ Estimate extended available water capacity from silt, clay, and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `silt` | % | — | Silt measured by particle-size method 2. | -| `clay` | % | — | Clay measured by particle-size method 2. | -| `organic_matter` | % | — | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `silt` | `number` | % | — | Silt measured by particle-size method 2. | +| `clay` | `number` | % | — | Clay measured by particle-size method 2. | +| `organic_matter` | `number` | % | — | Organic matter content. | #### Outputs @@ -332,9 +332,9 @@ Estimate extended available water capacity from sand. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | — | Sand measured by particle-size method 2. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | — | Sand measured by particle-size method 2. | #### Outputs @@ -354,10 +354,10 @@ Estimate extended available water capacity from sand and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | — | Sand measured by particle-size method 1. | -| `organic_matter` | % | — | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | — | Sand measured by particle-size method 1. | +| `organic_matter` | `number` | % | — | Organic matter content. | #### Outputs @@ -377,10 +377,10 @@ Estimate extended available water capacity from coarse sand and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `coarse_sand` | % | — | Coarse sand measured by particle-size method 1. | -| `organic_matter` | % | — | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `coarse_sand` | `number` | % | — | Coarse sand measured by particle-size method 1. | +| `organic_matter` | `number` | % | — | Organic matter content. | #### Outputs @@ -400,10 +400,10 @@ Estimate extended available water capacity from fine sand and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `fine_sand` | % | — | Fine sand measured by particle-size method 1. | -| `organic_matter` | % | — | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `fine_sand` | `number` | % | — | Fine sand measured by particle-size method 1. | +| `organic_matter` | `number` | % | — | Organic matter content. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/puckett1985.md b/docs/src/ptf-catalog/sources/puckett1985.md index 5771606..6323e4a 100644 --- a/docs/src/ptf-catalog/sources/puckett1985.md +++ b/docs/src/ptf-catalog/sources/puckett1985.md @@ -31,13 +31,13 @@ Estimate a point water-retention curve and saturated hydraulic conductivity. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 35 <= value <= 89 | Sand content, 0.05-2 mm. | -| `fine_sand` | % | — | Fine sand content, 0.106-0.25 mm. | -| `clay` | % | 1 <= value <= 42 | Clay content, <0.002 mm. | -| `bulk_density` | Mg/m^3 | 1.47 <= value <= 1.86 | Oven-dry bulk density. | -| `porosity` | cm^3/cm^3 | 0.30 <= value <= 0.48 | Porosity calculated from measured bulk and particle densities. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 35 <= value <= 89 | Sand content, 0.05-2 mm. | +| `fine_sand` | `number` | % | — | Fine sand content, 0.106-0.25 mm. | +| `clay` | `number` | % | 1 <= value <= 42 | Clay content, <0.002 mm. | +| `bulk_density` | `number` | Mg/m^3 | 1.47 <= value <= 1.86 | Oven-dry bulk density. | +| `porosity` | `number` | cm^3/cm^3 | 0.30 <= value <= 0.48 | Porosity calculated from measured bulk and particle densities. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/rawls1982.md b/docs/src/ptf-catalog/sources/rawls1982.md index 0c51102..18089f6 100644 --- a/docs/src/ptf-catalog/sources/rawls1982.md +++ b/docs/src/ptf-catalog/sources/rawls1982.md @@ -31,10 +31,10 @@ Estimate volumetric water content at -1500 kPa. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | 0.1 <= value <= 94 | Clay content. | -| `organic_matter` | % | 0.1 <= value <= 12.5 | Organic matter content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | 0.1 <= value <= 94 | Clay content. | +| `organic_matter` | `number` | % | 0.1 <= value <= 12.5 | Organic matter content. | #### Outputs @@ -54,11 +54,11 @@ Estimate volumetric water content at -33 kPa using measured or estimated theta_1 #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0.1 <= value <= 99 | Sand content. | -| `organic_matter` | % | 0.1 <= value <= 12.5 | Organic matter content. | -| `theta_1500` | cm^3/cm^3 | value >= 0 | Measured or estimated volumetric water content at -1500 kPa. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0.1 <= value <= 99 | Sand content. | +| `organic_matter` | `number` | % | 0.1 <= value <= 12.5 | Organic matter content. | +| `theta_1500` | `number` | cm^3/cm^3 | value >= 0 | Measured or estimated volumetric water content at -1500 kPa. | #### Outputs @@ -78,13 +78,13 @@ Estimate a twelve-point water-retention curve using theta_33 and theta_1500. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0.1 <= value <= 99 | Sand content. | -| `organic_matter` | % | 0.1 <= value <= 12.5 | Organic matter content. | -| `bulk_density` | g/cm^3 | 0.1 <= value <= 2.09 | Bulk density. | -| `theta_33` | cm^3/cm^3 | value >= 0 | Measured or estimated volumetric water content at -33 kPa. | -| `theta_1500` | cm^3/cm^3 | value >= 0 | Measured or estimated volumetric water content at -1500 kPa. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0.1 <= value <= 99 | Sand content. | +| `organic_matter` | `number` | % | 0.1 <= value <= 12.5 | Organic matter content. | +| `bulk_density` | `number` | g/cm^3 | 0.1 <= value <= 2.09 | Bulk density. | +| `theta_33` | `number` | cm^3/cm^3 | value >= 0 | Measured or estimated volumetric water content at -33 kPa. | +| `theta_1500` | `number` | cm^3/cm^3 | value >= 0 | Measured or estimated volumetric water content at -1500 kPa. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/saxton2006.md b/docs/src/ptf-catalog/sources/saxton2006.md index fde6aed..91aad9b 100644 --- a/docs/src/ptf-catalog/sources/saxton2006.md +++ b/docs/src/ptf-catalog/sources/saxton2006.md @@ -33,11 +33,11 @@ Estimate soil water characteristics from sand, clay, and organic matter. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | g/g | 0 <= value <= 1 | Sand mass fraction of the fine-earth soil. | -| `clay` | g/g | 0 <= value <= 0.60 | Clay mass fraction of the fine-earth soil. | -| `organic_matter` | % mass | 0 <= value <= 8 | Organic matter content on a mass percentage basis. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | g/g | 0 <= value <= 1 | Sand mass fraction of the fine-earth soil. | +| `clay` | `number` | g/g | 0 <= value <= 0.60 | Clay mass fraction of the fine-earth soil. | +| `organic_matter` | `number` | % mass | 0 <= value <= 8 | Organic matter content on a mass percentage basis. | #### Outputs @@ -86,12 +86,12 @@ Adjust Saxton and Rawls water characteristics for soil density. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `normal_density` | g/cm^3 | value > 0 | Normal dry bulk density estimated by the base model. | -| `theta_s` | m^3/m^3 | value > 0 | Saturated water content at normal density. | -| `theta_33` | m^3/m^3 | value > 0 | Water content at 33 kPa and normal density. | -| `density_factor` | dimensionless | 0.9 <= value <= 1.3 | Multiplicative adjustment to normal density. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `normal_density` | `number` | g/cm^3 | value > 0 | Normal dry bulk density estimated by the base model. | +| `theta_s` | `number` | m^3/m^3 | value > 0 | Saturated water content at normal density. | +| `theta_33` | `number` | m^3/m^3 | value > 0 | Water content at 33 kPa and normal density. | +| `density_factor` | `number` | dimensionless | 0.9 <= value <= 1.3 | Multiplicative adjustment to normal density. | #### Outputs @@ -122,11 +122,11 @@ Estimate matric tension in the 1500 to 33 kPa segment. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `theta` | m^3/m^3 | theta_1500 <= value <= theta_33 | Volumetric water content. | -| `theta_1500` | m^3/m^3 | value > 0 | Volumetric water content at 1500 kPa. | -| `theta_33` | m^3/m^3 | value > theta_1500 | Volumetric water content at 33 kPa. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `theta` | `number` | m^3/m^3 | theta_1500 <= value <= theta_33 | Volumetric water content. | +| `theta_1500` | `number` | m^3/m^3 | value > 0 | Volumetric water content at 1500 kPa. | +| `theta_33` | `number` | m^3/m^3 | value > theta_1500 | Volumetric water content at 33 kPa. | #### Outputs @@ -150,12 +150,12 @@ Estimate matric tension in the 33 kPa to air-entry segment. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `theta` | m^3/m^3 | theta_33 <= value <= theta_s | Volumetric water content. | -| `theta_33` | m^3/m^3 | value > 0 | Volumetric water content at 33 kPa. | -| `theta_s` | m^3/m^3 | value > theta_33 | Saturated volumetric water content. | -| `air_entry_tension` | kPa | 0 <= value < 33 | Air-entry tension estimated by the base model. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `theta` | `number` | m^3/m^3 | theta_33 <= value <= theta_s | Volumetric water content. | +| `theta_33` | `number` | m^3/m^3 | value > 0 | Volumetric water content at 33 kPa. | +| `theta_s` | `number` | m^3/m^3 | value > theta_33 | Saturated volumetric water content. | +| `air_entry_tension` | `number` | kPa | 0 <= value < 33 | Air-entry tension estimated by the base model. | #### Outputs @@ -183,12 +183,12 @@ Estimate unsaturated hydraulic conductivity from water content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `theta` | m^3/m^3 | 0 < value <= theta_s | Volumetric water content. | -| `theta_s` | m^3/m^3 | value > 0 | Saturated volumetric water content. | -| `saturated_conductivity` | mm/h | value >= 0 | Saturated hydraulic conductivity of the matric soil. | -| `conductivity_lambda` | dimensionless | value > 0 | Inverse of retention exponent B. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `theta` | `number` | m^3/m^3 | 0 < value <= theta_s | Volumetric water content. | +| `theta_s` | `number` | m^3/m^3 | value > 0 | Saturated volumetric water content. | +| `saturated_conductivity` | `number` | mm/h | value >= 0 | Saturated hydraulic conductivity of the matric soil. | +| `conductivity_lambda` | `number` | dimensionless | value > 0 | Inverse of retention exponent B. | #### Outputs @@ -212,12 +212,12 @@ Adjust matric-soil properties for gravel content. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `gravel_weight_fraction` | g/g | 0 <= value < 1 | Gravel mass fraction of the bulk soil. | -| `matric_density` | g/cm^3 | 0 < value <= 2.65 | Dry bulk density of the fine-earth matric soil. | -| `plant_available_water` | m^3/m^3 | value >= 0 | Plant-available water of the matric soil. | -| `saturated_conductivity` | mm/h | value >= 0 | Saturated hydraulic conductivity of the matric soil. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `gravel_weight_fraction` | `number` | g/g | 0 <= value < 1 | Gravel mass fraction of the bulk soil. | +| `matric_density` | `number` | g/cm^3 | 0 < value <= 2.65 | Dry bulk density of the fine-earth matric soil. | +| `plant_available_water` | `number` | m^3/m^3 | value >= 0 | Plant-available water of the matric soil. | +| `saturated_conductivity` | `number` | mm/h | value >= 0 | Saturated hydraulic conductivity of the matric soil. | #### Outputs @@ -252,11 +252,11 @@ Estimate saturated and moisture-adjusted osmotic potential. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `electrical_conductivity` | dS/m | value >= 0 | Electrical conductivity of a saturated soil extract. | -| `theta` | m^3/m^3 | 0 < value <= theta_s | Current volumetric water content. | -| `theta_s` | m^3/m^3 | value > 0 | Saturated volumetric water content. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `electrical_conductivity` | `number` | dS/m | value >= 0 | Electrical conductivity of a saturated soil extract. | +| `theta` | `number` | m^3/m^3 | 0 < value <= theta_s | Current volumetric water content. | +| `theta_s` | `number` | m^3/m^3 | value > 0 | Saturated volumetric water content. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/tiwary2014.md b/docs/src/ptf-catalog/sources/tiwary2014.md index 589dfcf..b565fc3 100644 --- a/docs/src/ptf-catalog/sources/tiwary2014.md +++ b/docs/src/ptf-catalog/sources/tiwary2014.md @@ -31,11 +31,11 @@ Estimate saturated conductivity for Indo-Gangetic Plains soils. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | — | Sand content. | -| `bulk_density` | Mg/m^3 | value > 0 | Bulk density. | -| `esp` | % | — | Exchangeable sodium percentage. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | — | Sand content. | +| `bulk_density` | `number` | Mg/m^3 | value > 0 | Bulk density. | +| `esp` | `number` | % | — | Exchangeable sodium percentage. | #### Outputs @@ -63,14 +63,14 @@ Estimate water retention and saturated conductivity for the black soil region. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `clay` | % | — | Clay content. | -| `ph` | dimensionless | value > 0 | Soil pH. | -| `cation_exchange_capacity` | cmol(c)/kg | — | Cation exchange capacity. | -| `esp` | % | — | Exchangeable sodium percentage. | -| `emp` | % | — | Exchangeable magnesium percentage. | -| `excm` | dimensionless | — | Exchangeable calcium-to-magnesium ratio. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `clay` | `number` | % | — | Clay content. | +| `ph` | `number` | dimensionless | value > 0 | Soil pH. | +| `cation_exchange_capacity` | `number` | cmol(c)/kg | — | Cation exchange capacity. | +| `esp` | `number` | % | — | Exchangeable sodium percentage. | +| `emp` | `number` | % | — | Exchangeable magnesium percentage. | +| `excm` | `number` | dimensionless | — | Exchangeable calcium-to-magnesium ratio. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/varallyai1982.md b/docs/src/ptf-catalog/sources/varallyai1982.md index 15f05bd..a54c565 100644 --- a/docs/src/ptf-catalog/sources/varallyai1982.md +++ b/docs/src/ptf-catalog/sources/varallyai1982.md @@ -31,11 +31,11 @@ Estimate equation (9) water-retention parameters for meadow-series soils. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `bulk_density` | g/cm^3 | value > 0 | Undisturbed-soil bulk density. | -| `fine_sand_fraction` | fraction | 0 <= value <= 1 | Mass fraction in the 0.25-0.05 mm particle-size class. | -| `fine_fraction` | fraction | 0 <= value <= 1 | Fine-particle mass fraction reported as particles smaller than 0.002 mm. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `bulk_density` | `number` | g/cm^3 | value > 0 | Undisturbed-soil bulk density. | +| `fine_sand_fraction` | `number` | fraction | 0 <= value <= 1 | Mass fraction in the 0.25-0.05 mm particle-size class. | +| `fine_fraction` | `number` | fraction | 0 <= value <= 1 | Fine-particle mass fraction reported as particles smaller than 0.002 mm. | #### Outputs @@ -77,10 +77,10 @@ Estimate equation (9) water-retention parameters for chernozem A horizons. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `bulk_density` | g/cm^3 | value > 0 | Undisturbed-soil bulk density. | -| `fine_fraction` | fraction | 0 <= value <= 1 | Fine-particle mass fraction reported as particles smaller than 0.002 mm. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `bulk_density` | `number` | g/cm^3 | value > 0 | Undisturbed-soil bulk density. | +| `fine_fraction` | `number` | fraction | 0 <= value <= 1 | Fine-particle mass fraction reported as particles smaller than 0.002 mm. | #### Outputs @@ -118,10 +118,10 @@ Estimate equation (9) water-retention parameters for chernozem B horizons. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `bulk_density` | g/cm^3 | value > 0 | Undisturbed-soil bulk density. | -| `fine_fraction` | fraction | 0 <= value <= 1 | Fine-particle mass fraction reported as particles smaller than 0.002 mm. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `bulk_density` | `number` | g/cm^3 | value > 0 | Undisturbed-soil bulk density. | +| `fine_fraction` | `number` | fraction | 0 <= value <= 1 | Fine-particle mass fraction reported as particles smaller than 0.002 mm. | #### Outputs @@ -159,10 +159,10 @@ Estimate equation (9) water-retention parameters for chernozem C horizons. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `bulk_density` | g/cm^3 | value > 0 | Undisturbed-soil bulk density. | -| `fine_fraction` | fraction | 0 <= value <= 1 | Fine-particle mass fraction reported as particles smaller than 0.002 mm. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `bulk_density` | `number` | g/cm^3 | value > 0 | Undisturbed-soil bulk density. | +| `fine_fraction` | `number` | fraction | 0 <= value <= 1 | Fine-particle mass fraction reported as particles smaller than 0.002 mm. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/vereecken1989.md b/docs/src/ptf-catalog/sources/vereecken1989.md index 39b9f07..6902179 100644 --- a/docs/src/ptf-catalog/sources/vereecken1989.md +++ b/docs/src/ptf-catalog/sources/vereecken1989.md @@ -33,12 +33,12 @@ Estimate four parameters of the reduced van Genuchten moisture-retention model f #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 5.60 <= value <= 97.80 | Sand content in the 50-2000 micrometre particle-size fraction. | -| `clay` | % | 0 <= value <= 54.46 | Clay content in the less-than-2-micrometre particle-size fraction. | -| `carbon` | % | 0.01 <= value <= 6.60 | Carbon content determined by the Walkley-Black method. | -| `bulk_density` | g/cm^3 | 1.040 <= value <= 1.730 | Dry bulk density measured after drying undisturbed 100-cm^3 cores for 24 hours at 105 degrees Celsius. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 5.60 <= value <= 97.80 | Sand content in the 50-2000 micrometre particle-size fraction. | +| `clay` | `number` | % | 0 <= value <= 54.46 | Clay content in the less-than-2-micrometre particle-size fraction. | +| `carbon` | `number` | % | 0.01 <= value <= 6.60 | Carbon content determined by the Walkley-Black method. | +| `bulk_density` | `number` | g/cm^3 | 1.040 <= value <= 1.730 | Dry bulk density measured after drying undisturbed 100-cm^3 cores for 24 hours at 105 degrees Celsius. | #### Outputs @@ -73,21 +73,21 @@ Estimate van Genuchten parameters from nine particle-size fractions and particle #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `particle_2000_1000` | % | 0 <= value <= 4.50 | Particle content from 2000 to 1000 micrometres. | -| `particle_1000_500` | % | 0 <= value <= 9.30 | Particle content from 1000 to 500 micrometres. | -| `particle_500_200` | % | 0.10 <= value <= 85.70 | Particle content from 500 to 200 micrometres. | -| `particle_200_100` | % | 0.10 <= value <= 68.90 | Particle content from 200 to 100 micrometres. | -| `particle_100_50` | % | 0.70 <= value <= 51.00 | Particle content from 100 to 50 micrometres. | -| `particle_50_20` | % | 0 <= value <= 62.80 | Particle content from 50 to 20 micrometres. | -| `particle_20_10` | % | 0 <= value <= 19.00 | Particle content from 20 to 10 micrometres. | -| `particle_10_2` | % | 0 <= value <= 23.33 | Particle content from 10 to 2 micrometres. | -| `clay` | % | 0 <= value <= 54.46 | Particle content below 2 micrometres. | -| `geometric_mean_particle_size` | cm | value > 0 | Geometrical mean particle size calculated using the method cited by the paper. | -| `geometric_standard_deviation` | dimensionless | value > 0 | Geometrical standard deviation of particle size calculated using the method cited by the paper. | -| `carbon` | % | 0.01 <= value <= 6.60 | Carbon content determined by the Walkley-Black method. | -| `bulk_density` | g/cm^3 | 1.040 <= value <= 1.730 | Dry bulk density measured after drying undisturbed 100-cm^3 cores for 24 hours at 105 degrees Celsius. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `particle_2000_1000` | `number` | % | 0 <= value <= 4.50 | Particle content from 2000 to 1000 micrometres. | +| `particle_1000_500` | `number` | % | 0 <= value <= 9.30 | Particle content from 1000 to 500 micrometres. | +| `particle_500_200` | `number` | % | 0.10 <= value <= 85.70 | Particle content from 500 to 200 micrometres. | +| `particle_200_100` | `number` | % | 0.10 <= value <= 68.90 | Particle content from 200 to 100 micrometres. | +| `particle_100_50` | `number` | % | 0.70 <= value <= 51.00 | Particle content from 100 to 50 micrometres. | +| `particle_50_20` | `number` | % | 0 <= value <= 62.80 | Particle content from 50 to 20 micrometres. | +| `particle_20_10` | `number` | % | 0 <= value <= 19.00 | Particle content from 20 to 10 micrometres. | +| `particle_10_2` | `number` | % | 0 <= value <= 23.33 | Particle content from 10 to 2 micrometres. | +| `clay` | `number` | % | 0 <= value <= 54.46 | Particle content below 2 micrometres. | +| `geometric_mean_particle_size` | `number` | cm | value > 0 | Geometrical mean particle size calculated using the method cited by the paper. | +| `geometric_standard_deviation` | `number` | dimensionless | value > 0 | Geometrical standard deviation of particle size calculated using the method cited by the paper. | +| `carbon` | `number` | % | 0.01 <= value <= 6.60 | Carbon content determined by the Walkley-Black method. | +| `bulk_density` | `number` | g/cm^3 | 1.040 <= value <= 1.730 | Dry bulk density measured after drying undisturbed 100-cm^3 cores for 24 hours at 105 degrees Celsius. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/wang2012.md b/docs/src/ptf-catalog/sources/wang2012.md index 3b41b08..798b776 100644 --- a/docs/src/ptf-catalog/sources/wang2012.md +++ b/docs/src/ptf-catalog/sources/wang2012.md @@ -33,14 +33,14 @@ Estimate saturated water content, field capacity, and saturated conductivity. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `sand` | % | 0.8 <= value <= 92.0 | Sand content, 0.05-1 mm. | -| `silt` | % | 6.3 <= value <= 84.7 | Silt content, 0.002-0.05 mm. | -| `clay` | % | 0.4 <= value <= 35.8 | Clay content, <0.002 mm. | -| `bulk_density` | g/cm^3 | 1.04 <= value <= 1.82 | Bulk density. | -| `soil_organic_carbon` | % | 0.033 <= value <= 3.369 | Soil organic carbon exposed by the public API as percent by mass. | -| `altitude` | m | 99 <= value <= 2835 | Altitude above sea level. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `sand` | `number` | % | 0.8 <= value <= 92.0 | Sand content, 0.05-1 mm. | +| `silt` | `number` | % | 6.3 <= value <= 84.7 | Silt content, 0.002-0.05 mm. | +| `clay` | `number` | % | 0.4 <= value <= 35.8 | Clay content, <0.002 mm. | +| `bulk_density` | `number` | g/cm^3 | 1.04 <= value <= 1.82 | Bulk density. | +| `soil_organic_carbon` | `number` | % | 0.033 <= value <= 3.369 | Soil organic carbon exposed by the public API as percent by mass. | +| `altitude` | `number` | m | 99 <= value <= 2835 | Altitude above sea level. | #### Outputs diff --git a/docs/src/ptf-catalog/sources/weber2020.md b/docs/src/ptf-catalog/sources/weber2020.md index 19cae44..5c1e834 100644 --- a/docs/src/ptf-catalog/sources/weber2020.md +++ b/docs/src/ptf-catalog/sources/weber2020.md @@ -33,14 +33,14 @@ Convert VGM parameters to Brunswick-VGM parameters. #### Inputs -| Name | Unit | Domain | Description | -| --- | --- | --- | --- | -| `theta_r_vgm` | dimensionless | 0.001 <= value <= 0.35 | VGM residual volumetric water content. | -| `theta_s_vgm` | dimensionless | 0.2 <= value <= 0.7 | VGM saturated volumetric water content. | -| `alpha_vgm` | cm^-1 | 0.001 <= value <= 0.1 | VGM inverse pressure-head scale parameter. | -| `n_vgm` | dimensionless | 1.1 <= value <= 11 | VGM pore-size distribution shape parameter. | -| `tau_vgm` | dimensionless | -2 <= value <= 10 | VGM hydraulic-conductivity shape parameter. | -| `k_s_vgm` | cm d^-1 | 1 <= value <= 1000 | VGM saturated hydraulic conductivity. | +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `theta_r_vgm` | `number` | dimensionless | 0.001 <= value <= 0.35 | VGM residual volumetric water content. | +| `theta_s_vgm` | `number` | dimensionless | 0.2 <= value <= 0.7 | VGM saturated volumetric water content. | +| `alpha_vgm` | `number` | cm^-1 | 0.001 <= value <= 0.1 | VGM inverse pressure-head scale parameter. | +| `n_vgm` | `number` | dimensionless | 1.1 <= value <= 11 | VGM pore-size distribution shape parameter. | +| `tau_vgm` | `number` | dimensionless | -2 <= value <= 10 | VGM hydraulic-conductivity shape parameter. | +| `k_s_vgm` | `number` | cm d^-1 | 1 <= value <= 1000 | VGM saturated hydraulic conductivity. | #### Outputs diff --git a/specs/schema/ptf-spec.schema.json b/specs/schema/ptf-spec.schema.json index 494f6ff..3ba4f40 100644 --- a/specs/schema/ptf-spec.schema.json +++ b/specs/schema/ptf-spec.schema.json @@ -29,13 +29,34 @@ } }, "implementationVariable": { - "type": "object", - "additionalProperties": false, - "required": ["name", "expr"], - "properties": { - "name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, - "expr": { "type": "string", "minLength": 1 } - } + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["name", "expr"], + "properties": { + "name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, + "expr": { "type": "string", "minLength": 1 } + } + }, + { + "type": "object", + "additionalProperties": false, + "required": ["name", "lookup"], + "properties": { + "name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, + "lookup": { + "type": "object", + "additionalProperties": false, + "required": ["table", "key"], + "properties": { + "table": { "$ref": "#/$defs/localReference" }, + "key": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" } + } + } + } + } + ] }, "implementation": { "type": "object", @@ -49,6 +70,34 @@ } } }, + "lookupDefinition": { + "type": "object", + "additionalProperties": false, + "required": ["type", "input", "output", "values"], + "properties": { + "type": { "const": "lookup" }, + "input": { "$ref": "#/$defs/localReference" }, + "output": { "$ref": "#/$defs/localReference" }, + "values": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/lookupValue" } + } + } + }, + "lookupValue": { + "type": "object", + "additionalProperties": false, + "required": ["key", "value"], + "properties": { + "key": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, + "value": { + "type": "object", + "minProperties": 1, + "additionalProperties": { "type": "number" } + } + } + }, "stringOrNull": { "type": ["string", "null"] }, "localReference": { "type": "object", @@ -122,13 +171,52 @@ "description": { "type": "string", "minLength": 1 } } }, + "inputReference": { + "type": "object", + "additionalProperties": false, + "required": ["name", "$ref"], + "properties": { + "name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, + "description": { "type": "string", "minLength": 1 }, + "$ref": { + "type": "string", + "pattern": "^#/\\$defs/[A-Za-z][A-Za-z0-9_]*$" + } + } + }, + "enumDefinition": { + "type": "object", + "additionalProperties": false, + "required": ["type", "description", "values"], + "properties": { + "type": { "const": "enum" }, + "description": { "type": "string", "minLength": 1 }, + "values": { + "type": "array", + "minItems": 1, + "items": { "$ref": "#/$defs/enumValue" } + } + } + }, + "enumValue": { + "type": "object", + "additionalProperties": false, + "required": ["name", "value"], + "properties": { + "name": { "type": "string", "pattern": "^[a-z][a-z0-9_]*$" }, + "value": { "type": "string", "minLength": 1 }, + "description": { "type": "string", "minLength": 1 } + } + }, "sourceDefinitions": { "type": "object", "propertyNames": { "pattern": "^[A-Za-z][A-Za-z0-9_]*$" }, "additionalProperties": { "oneOf": [ - { "$ref": "#/$defs/parameter" }, - { "$ref": "#/$defs/outputs" } + { "$ref": "#/$defs/enumDefinition" }, + { "$ref": "#/$defs/lookupDefinition" }, + { "$ref": "#/$defs/outputs" }, + { "$ref": "#/$defs/parameter" } ] } }, @@ -245,6 +333,7 @@ "items": { "oneOf": [ { "$ref": "#/$defs/parameter" }, + { "$ref": "#/$defs/inputReference" }, { "$ref": "#/$defs/localReference" } ] } diff --git a/targets/ptfkit-native/cpp/CMakeLists.txt b/targets/ptfkit-native/cpp/CMakeLists.txt index 9ce1908..0ff1e71 100644 --- a/targets/ptfkit-native/cpp/CMakeLists.txt +++ b/targets/ptfkit-native/cpp/CMakeLists.txt @@ -3,7 +3,7 @@ add_library(ptfkit::cpp ALIAS ptfkit_cpp) set_target_properties(ptfkit_cpp PROPERTIES EXPORT_NAME cpp CXX_SCAN_FOR_MODULES ON ) -target_compile_features(ptfkit_cpp PUBLIC cxx_std_20) +target_compile_features(ptfkit_cpp PUBLIC cxx_std_23) target_compile_definitions(ptfkit_cpp PUBLIC PTFKIT_VERSION=\"${PROJECT_VERSION}\" ) diff --git a/targets/ptfkit-native/include/ptfkit/detail/record.h b/targets/ptfkit-native/include/ptfkit/detail/record.h new file mode 100644 index 0000000..5d9c861 --- /dev/null +++ b/targets/ptfkit-native/include/ptfkit/detail/record.h @@ -0,0 +1,12 @@ +#ifndef PTFKIT_DETAIL_RECORD_H +#define PTFKIT_DETAIL_RECORD_H + +#ifdef __cplusplus +#define PTFKIT_RECORD_LITERAL(type, ...) \ + type { __VA_ARGS__ } +#else +#define PTFKIT_RECORD_LITERAL(type, ...) \ + (type) { __VA_ARGS__ } +#endif + +#endif diff --git a/targets/ptfkit-py/src/ptfkit/_dispatch.py b/targets/ptfkit-py/src/ptfkit/_dispatch.py index b812004..f99281a 100644 --- a/targets/ptfkit-py/src/ptfkit/_dispatch.py +++ b/targets/ptfkit-py/src/ptfkit/_dispatch.py @@ -2,11 +2,11 @@ from typing import Any -from numpy import asarray, float64, ufunc +from numpy import asarray, ufunc def call(function: ufunc, *inputs: object, out: object) -> Any: # noqa: ANN401 - inputs = tuple(asarray(value, dtype=float64) for value in inputs) + inputs = tuple(asarray(value) for value in inputs) if out is None: return function(*inputs) if isinstance(out, tuple): diff --git a/targets/ptfkit-py/src/ptfkit/ahuja1984.c b/targets/ptfkit-py/src/ptfkit/ahuja1984.c index bfda466..922a1c5 100644 --- a/targets/ptfkit-py/src/ptfkit/ahuja1984.c +++ b/targets/ptfkit-py/src/ptfkit/ahuja1984.c @@ -2,6 +2,8 @@ #include #include "ufunc.h" +static const int calc_ptf_ahuja1984_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_ahuja1984_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -55,7 +57,8 @@ static PyArrayMethod_Spec calc_ptf_ahuja1984_spec = { }; int ptfkit_register_ahuja1984(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_ahuja1984", 4, 1, &calc_ptf_ahuja1984_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_ahuja1984", calc_ptf_ahuja1984_types, 4, 1, + &calc_ptf_ahuja1984_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/aimrun2009.c b/targets/ptfkit-py/src/ptfkit/aimrun2009.c index eb7ab8e..0b968e9 100644 --- a/targets/ptfkit-py/src/ptfkit/aimrun2009.c +++ b/targets/ptfkit-py/src/ptfkit/aimrun2009.c @@ -2,6 +2,8 @@ #include #include "ufunc.h" +static const int calc_ptf_aimrun2009_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_aimrun2009_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -53,7 +55,8 @@ static PyArrayMethod_Spec calc_ptf_aimrun2009_spec = { }; int ptfkit_register_aimrun2009(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_aimrun2009", 4, 1, &calc_ptf_aimrun2009_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_aimrun2009", calc_ptf_aimrun2009_types, 4, 1, + &calc_ptf_aimrun2009_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/beniaich2023.c b/targets/ptfkit-py/src/ptfkit/beniaich2023.c index 09e5095..f83da68 100644 --- a/targets/ptfkit-py/src/ptfkit/beniaich2023.c +++ b/targets/ptfkit-py/src/ptfkit/beniaich2023.c @@ -2,6 +2,8 @@ #include #include "ufunc.h" +static const int calc_ptf_beniaich2023_slr1_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_beniaich2023_slr1_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -51,6 +53,8 @@ static PyArrayMethod_Spec calc_ptf_beniaich2023_slr1_spec = { .slots = calc_ptf_beniaich2023_slr1_slots, }; +static const int calc_ptf_beniaich2023_slr2_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_beniaich2023_slr2_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -100,6 +104,8 @@ static PyArrayMethod_Spec calc_ptf_beniaich2023_slr2_spec = { .slots = calc_ptf_beniaich2023_slr2_slots, }; +static const int calc_ptf_beniaich2023_slr3_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_beniaich2023_slr3_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -149,6 +155,8 @@ static PyArrayMethod_Spec calc_ptf_beniaich2023_slr3_spec = { .slots = calc_ptf_beniaich2023_slr3_slots, }; +static const int calc_ptf_beniaich2023_slr4_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_beniaich2023_slr4_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -201,6 +209,8 @@ static PyArrayMethod_Spec calc_ptf_beniaich2023_slr4_spec = { .slots = calc_ptf_beniaich2023_slr4_slots, }; +static const int calc_ptf_beniaich2023_slr5_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_beniaich2023_slr5_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -253,6 +263,8 @@ static PyArrayMethod_Spec calc_ptf_beniaich2023_slr5_spec = { .slots = calc_ptf_beniaich2023_slr5_slots, }; +static const int calc_ptf_beniaich2023_slr6_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_beniaich2023_slr6_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -304,6 +316,8 @@ static PyArrayMethod_Spec calc_ptf_beniaich2023_slr6_spec = { .slots = calc_ptf_beniaich2023_slr6_slots, }; +static const int calc_ptf_beniaich2023_mlr1_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_beniaich2023_mlr1_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -361,6 +375,8 @@ static PyArrayMethod_Spec calc_ptf_beniaich2023_mlr1_spec = { .slots = calc_ptf_beniaich2023_mlr1_slots, }; +static const int calc_ptf_beniaich2023_mlr2_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_beniaich2023_mlr2_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -415,6 +431,8 @@ static PyArrayMethod_Spec calc_ptf_beniaich2023_mlr2_spec = { .slots = calc_ptf_beniaich2023_mlr2_slots, }; +static const int calc_ptf_beniaich2023_mlr3_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_beniaich2023_mlr3_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -469,6 +487,8 @@ static PyArrayMethod_Spec calc_ptf_beniaich2023_mlr3_spec = { .slots = calc_ptf_beniaich2023_mlr3_slots, }; +static const int calc_ptf_beniaich2023_mlr4_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_beniaich2023_mlr4_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -523,6 +543,8 @@ static PyArrayMethod_Spec calc_ptf_beniaich2023_mlr4_spec = { .slots = calc_ptf_beniaich2023_mlr4_slots, }; +static const int calc_ptf_beniaich2023_mlr5_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_beniaich2023_mlr5_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -581,38 +603,38 @@ static PyArrayMethod_Spec calc_ptf_beniaich2023_mlr5_spec = { }; int ptfkit_register_beniaich2023(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr1", 1, 3, - &calc_ptf_beniaich2023_slr1_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr1", calc_ptf_beniaich2023_slr1_types, 1, + 3, &calc_ptf_beniaich2023_slr1_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr2", 1, 3, - &calc_ptf_beniaich2023_slr2_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr2", calc_ptf_beniaich2023_slr2_types, 1, + 3, &calc_ptf_beniaich2023_slr2_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr3", 1, 3, - &calc_ptf_beniaich2023_slr3_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr3", calc_ptf_beniaich2023_slr3_types, 1, + 3, &calc_ptf_beniaich2023_slr3_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr4", 2, 3, - &calc_ptf_beniaich2023_slr4_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr4", calc_ptf_beniaich2023_slr4_types, 2, + 3, &calc_ptf_beniaich2023_slr4_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr5", 2, 3, - &calc_ptf_beniaich2023_slr5_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr5", calc_ptf_beniaich2023_slr5_types, 2, + 3, &calc_ptf_beniaich2023_slr5_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr6", 1, 3, - &calc_ptf_beniaich2023_slr6_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_slr6", calc_ptf_beniaich2023_slr6_types, 1, + 3, &calc_ptf_beniaich2023_slr6_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_mlr1", 3, 3, - &calc_ptf_beniaich2023_mlr1_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_mlr1", calc_ptf_beniaich2023_mlr1_types, 3, + 3, &calc_ptf_beniaich2023_mlr1_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_mlr2", 2, 3, - &calc_ptf_beniaich2023_mlr2_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_mlr2", calc_ptf_beniaich2023_mlr2_types, 2, + 3, &calc_ptf_beniaich2023_mlr2_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_mlr3", 2, 3, - &calc_ptf_beniaich2023_mlr3_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_mlr3", calc_ptf_beniaich2023_mlr3_types, 2, + 3, &calc_ptf_beniaich2023_mlr3_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_mlr4", 2, 3, - &calc_ptf_beniaich2023_mlr4_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_mlr4", calc_ptf_beniaich2023_mlr4_types, 2, + 3, &calc_ptf_beniaich2023_mlr4_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_mlr5", 3, 3, - &calc_ptf_beniaich2023_mlr5_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_beniaich2023_mlr5", calc_ptf_beniaich2023_mlr5_types, 3, + 3, &calc_ptf_beniaich2023_mlr5_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/chakraborty2011.c b/targets/ptfkit-py/src/ptfkit/chakraborty2011.c index d66f6ac..48d33e9 100644 --- a/targets/ptfkit-py/src/ptfkit/chakraborty2011.c +++ b/targets/ptfkit-py/src/ptfkit/chakraborty2011.c @@ -2,6 +2,8 @@ #include #include "ufunc.h" +static const int calc_ptf_chakraborty2011_eq1_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_chakraborty2011_eq1_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -58,6 +60,8 @@ static PyArrayMethod_Spec calc_ptf_chakraborty2011_eq1_spec = { .slots = calc_ptf_chakraborty2011_eq1_slots, }; +static const int calc_ptf_chakraborty2011_eq2_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_chakraborty2011_eq2_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -116,6 +120,8 @@ static PyArrayMethod_Spec calc_ptf_chakraborty2011_eq2_spec = { .slots = calc_ptf_chakraborty2011_eq2_slots, }; +static const int calc_ptf_chakraborty2011_eq3_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_chakraborty2011_eq3_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -177,6 +183,8 @@ static PyArrayMethod_Spec calc_ptf_chakraborty2011_eq3_spec = { .slots = calc_ptf_chakraborty2011_eq3_slots, }; +static const int calc_ptf_chakraborty2011_eq4_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_chakraborty2011_eq4_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -238,6 +246,8 @@ static PyArrayMethod_Spec calc_ptf_chakraborty2011_eq4_spec = { .slots = calc_ptf_chakraborty2011_eq4_slots, }; +static const int calc_ptf_chakraborty2011_eq5_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_chakraborty2011_eq5_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -302,6 +312,9 @@ static PyArrayMethod_Spec calc_ptf_chakraborty2011_eq5_spec = { .slots = calc_ptf_chakraborty2011_eq5_slots, }; +static const int calc_ptf_chakraborty2011_eq6_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_chakraborty2011_eq6_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -370,23 +383,23 @@ static PyArrayMethod_Spec calc_ptf_chakraborty2011_eq6_spec = { }; int ptfkit_register_chakraborty2011(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq1", 2, 4, - &calc_ptf_chakraborty2011_eq1_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq1", calc_ptf_chakraborty2011_eq1_types, + 2, 4, &calc_ptf_chakraborty2011_eq1_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq2", 2, 4, - &calc_ptf_chakraborty2011_eq2_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq2", calc_ptf_chakraborty2011_eq2_types, + 2, 4, &calc_ptf_chakraborty2011_eq2_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq3", 3, 4, - &calc_ptf_chakraborty2011_eq3_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq3", calc_ptf_chakraborty2011_eq3_types, + 3, 4, &calc_ptf_chakraborty2011_eq3_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq4", 3, 4, - &calc_ptf_chakraborty2011_eq4_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq4", calc_ptf_chakraborty2011_eq4_types, + 3, 4, &calc_ptf_chakraborty2011_eq4_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq5", 4, 4, - &calc_ptf_chakraborty2011_eq5_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq5", calc_ptf_chakraborty2011_eq5_types, + 4, 4, &calc_ptf_chakraborty2011_eq5_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq6", 5, 4, - &calc_ptf_chakraborty2011_eq6_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_chakraborty2011_eq6", calc_ptf_chakraborty2011_eq6_types, + 5, 4, &calc_ptf_chakraborty2011_eq6_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/cosby1984.c b/targets/ptfkit-py/src/ptfkit/cosby1984.c index 82c2df3..49e090a 100644 --- a/targets/ptfkit-py/src/ptfkit/cosby1984.c +++ b/targets/ptfkit-py/src/ptfkit/cosby1984.c @@ -2,6 +2,9 @@ #include #include "ufunc.h" +static const int calc_ptf_cosby1984_univariate_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_cosby1984_univariate_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -73,7 +76,8 @@ static PyArrayMethod_Spec calc_ptf_cosby1984_univariate_spec = { }; int ptfkit_register_cosby1984(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_cosby1984_univariate", 3, 7, + if (ptfkit_add_ufunc(module, "calc_ptf_cosby1984_univariate", + calc_ptf_cosby1984_univariate_types, 3, 7, &calc_ptf_cosby1984_univariate_spec) < 0) return -1; return 0; diff --git a/targets/ptfkit-py/src/ptfkit/dharumarajan2019.c b/targets/ptfkit-py/src/ptfkit/dharumarajan2019.c index 852e214..abe1c59 100644 --- a/targets/ptfkit-py/src/ptfkit/dharumarajan2019.c +++ b/targets/ptfkit-py/src/ptfkit/dharumarajan2019.c @@ -2,6 +2,8 @@ #include #include "ufunc.h" +static const int calc_ptf_dharumarajan2019_nkp_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_dharumarajan2019_nkp_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -57,6 +59,7 @@ static PyArrayMethod_Spec calc_ptf_dharumarajan2019_nkp_spec = { .slots = calc_ptf_dharumarajan2019_nkp_slots, }; +static const int calc_ptf_dharumarajan2019_nkp_clay_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_dharumarajan2019_nkp_clay_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -107,6 +110,8 @@ static PyArrayMethod_Spec calc_ptf_dharumarajan2019_nkp_clay_spec = { .slots = calc_ptf_dharumarajan2019_nkp_clay_slots, }; +static const int calc_ptf_dharumarajan2019_skp_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_dharumarajan2019_skp_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -162,6 +167,7 @@ static PyArrayMethod_Spec calc_ptf_dharumarajan2019_skp_spec = { .slots = calc_ptf_dharumarajan2019_skp_slots, }; +static const int calc_ptf_dharumarajan2019_skp_clay_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_dharumarajan2019_skp_clay_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -212,6 +218,8 @@ static PyArrayMethod_Spec calc_ptf_dharumarajan2019_skp_clay_spec = { .slots = calc_ptf_dharumarajan2019_skp_clay_slots, }; +static const int calc_ptf_dharumarajan2019_infiltration_types[] = {NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_dharumarajan2019_infiltration_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -264,19 +272,24 @@ static PyArrayMethod_Spec calc_ptf_dharumarajan2019_infiltration_spec = { }; int ptfkit_register_dharumarajan2019(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_dharumarajan2019_nkp", 3, 2, + if (ptfkit_add_ufunc(module, "calc_ptf_dharumarajan2019_nkp", + calc_ptf_dharumarajan2019_nkp_types, 3, 2, &calc_ptf_dharumarajan2019_nkp_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_dharumarajan2019_nkp_clay", 1, 2, + if (ptfkit_add_ufunc(module, "calc_ptf_dharumarajan2019_nkp_clay", + calc_ptf_dharumarajan2019_nkp_clay_types, 1, 2, &calc_ptf_dharumarajan2019_nkp_clay_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_dharumarajan2019_skp", 3, 2, + if (ptfkit_add_ufunc(module, "calc_ptf_dharumarajan2019_skp", + calc_ptf_dharumarajan2019_skp_types, 3, 2, &calc_ptf_dharumarajan2019_skp_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_dharumarajan2019_skp_clay", 1, 2, + if (ptfkit_add_ufunc(module, "calc_ptf_dharumarajan2019_skp_clay", + calc_ptf_dharumarajan2019_skp_clay_types, 1, 2, &calc_ptf_dharumarajan2019_skp_clay_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_dharumarajan2019_infiltration", 3, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_dharumarajan2019_infiltration", + calc_ptf_dharumarajan2019_infiltration_types, 3, 1, &calc_ptf_dharumarajan2019_infiltration_spec) < 0) return -1; return 0; diff --git a/targets/ptfkit-py/src/ptfkit/enums.py b/targets/ptfkit-py/src/ptfkit/enums.py new file mode 100644 index 0000000..4e8a206 --- /dev/null +++ b/targets/ptfkit-py/src/ptfkit/enums.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING, Generic, TypeVar + +import numpy as np + + +if TYPE_CHECKING: + from collections.abc import Iterable + + from numpy.typing import NDArray + + +E = TypeVar('E', bound=Enum) + + +class EnumArray(Generic[E]): + """A typed wrapper around an encoded NumPy enum array.""" + + __slots__ = ('_codes', '_enum_type') + _codes: NDArray[np.uint32] + _enum_type: type[E] + + def __init__(self) -> None: + """Reject direct construction without typed enum members.""" + message = 'construct enum arrays with EnumType.array(...)' + raise TypeError(message) + + @classmethod + def _from_members(cls, enum_type: type[E], values: Iterable[E]) -> EnumArray[E]: + members = {member: ordinal for ordinal, member in enumerate(enum_type)} + + def ordinal(value: E) -> int: + if not isinstance(value, enum_type): + message = f'expected a {enum_type.__name__} member, got {type(value).__name__}' + raise TypeError(message) + return members[value] + + codes = np.fromiter((ordinal(value) for value in values), dtype=np.uint32) + instance = object.__new__(cls) + instance._enum_type = enum_type # noqa: SLF001 + instance._codes = codes # noqa: SLF001 + return instance + + @staticmethod + def _encode_member(enum_type: type[E], value: E) -> np.uint32: + for ordinal, member in enumerate(enum_type): + if value is member: + return np.uint32(ordinal) + message = f'expected a {enum_type.__name__} member' + raise TypeError(message) + + def _codes_for(self, enum_type: type[E]) -> NDArray[np.uint32]: + if self._enum_type is not enum_type: + message = ( + f'expected EnumArray[{enum_type.__name__}], ' + f'got EnumArray[{self._enum_type.__name__}]' + ) + raise TypeError(message) + return self._codes diff --git a/targets/ptfkit-py/src/ptfkit/ferrerjulia2004.c b/targets/ptfkit-py/src/ptfkit/ferrerjulia2004.c index 6d8c810..490acee 100644 --- a/targets/ptfkit-py/src/ptfkit/ferrerjulia2004.c +++ b/targets/ptfkit-py/src/ptfkit/ferrerjulia2004.c @@ -2,6 +2,8 @@ #include #include "ufunc.h" +static const int calc_ptf_ferrerjulia2004_campbell_shiozawa_types[] = {NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_campbell_shiozawa_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -48,6 +50,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_campbell_shiozawa_spec = { .slots = calc_ptf_ferrerjulia2004_campbell_shiozawa_slots, }; +static const int calc_ptf_ferrerjulia2004_saxton_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_saxton_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -96,6 +99,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_saxton_spec = { .slots = calc_ptf_ferrerjulia2004_saxton_slots, }; +static const int calc_ptf_ferrerjulia2004_dane_puckett_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_dane_puckett_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -141,6 +145,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_dane_puckett_spec = { .slots = calc_ptf_ferrerjulia2004_dane_puckett_slots, }; +static const int calc_ptf_ferrerjulia2004_puckett_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_puckett_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -186,6 +191,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_puckett_spec = { .slots = calc_ptf_ferrerjulia2004_puckett_slots, }; +static const int calc_ptf_ferrerjulia2004_cosby_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_cosby_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -234,6 +240,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_cosby_spec = { .slots = calc_ptf_ferrerjulia2004_cosby_slots, }; +static const int calc_ptf_ferrerjulia2004_humic_acrisol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_humic_acrisol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -277,6 +284,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_humic_acrisol_sand_spec = { .slots = calc_ptf_ferrerjulia2004_humic_acrisol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_humic_acrisol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_humic_acrisol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -328,6 +337,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_humic_acrisol_texture_organic .slots = calc_ptf_ferrerjulia2004_humic_acrisol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_calcic_cambisol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_calcic_cambisol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -369,6 +379,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_calcic_cambisol_sand_spec = { .slots = calc_ptf_ferrerjulia2004_calcic_cambisol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_calcic_cambisol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_calcic_cambisol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -422,6 +434,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_calcic_cambisol_texture_organ .slots = calc_ptf_ferrerjulia2004_calcic_cambisol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_dystric_cambisol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_dystric_cambisol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -463,6 +476,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_dystric_cambisol_sand_spec = .slots = calc_ptf_ferrerjulia2004_dystric_cambisol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_dystric_cambisol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_dystric_cambisol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -516,6 +531,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_dystric_cambisol_texture_orga .slots = calc_ptf_ferrerjulia2004_dystric_cambisol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_eutric_cambisol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_eutric_cambisol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -557,6 +573,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_eutric_cambisol_sand_spec = { .slots = calc_ptf_ferrerjulia2004_eutric_cambisol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_eutric_cambisol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_eutric_cambisol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -610,6 +628,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_eutric_cambisol_texture_organ .slots = calc_ptf_ferrerjulia2004_eutric_cambisol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_gleyic_cambisol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_gleyic_cambisol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -651,6 +670,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_gleyic_cambisol_sand_spec = { .slots = calc_ptf_ferrerjulia2004_gleyic_cambisol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_gleyic_cambisol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_gleyic_cambisol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -704,6 +725,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_gleyic_cambisol_texture_organ .slots = calc_ptf_ferrerjulia2004_gleyic_cambisol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_humic_cambisol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_humic_cambisol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -747,6 +769,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_humic_cambisol_sand_spec = { .slots = calc_ptf_ferrerjulia2004_humic_cambisol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_humic_cambisol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_humic_cambisol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -798,6 +822,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_humic_cambisol_texture_organi .slots = calc_ptf_ferrerjulia2004_humic_cambisol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_calcaric_fluvisol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_calcaric_fluvisol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -839,6 +864,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_calcaric_fluvisol_sand_spec = .slots = calc_ptf_ferrerjulia2004_calcaric_fluvisol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_calcaric_fluvisol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_calcaric_fluvisol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -892,6 +919,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_calcaric_fluvisol_texture_org .slots = calc_ptf_ferrerjulia2004_calcaric_fluvisol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_calcic_luvisol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_calcic_luvisol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -935,6 +963,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_calcic_luvisol_sand_spec = { .slots = calc_ptf_ferrerjulia2004_calcic_luvisol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_calcic_luvisol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_calcic_luvisol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -986,6 +1016,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_calcic_luvisol_texture_organi .slots = calc_ptf_ferrerjulia2004_calcic_luvisol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_chromic_luvisol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_chromic_luvisol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1027,6 +1058,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_chromic_luvisol_sand_spec = { .slots = calc_ptf_ferrerjulia2004_chromic_luvisol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_chromic_luvisol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_chromic_luvisol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1080,6 +1113,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_chromic_luvisol_texture_organ .slots = calc_ptf_ferrerjulia2004_chromic_luvisol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_gleyic_luvisol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_gleyic_luvisol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1123,6 +1157,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_gleyic_luvisol_sand_spec = { .slots = calc_ptf_ferrerjulia2004_gleyic_luvisol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_gleyic_luvisol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_gleyic_luvisol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1174,6 +1210,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_gleyic_luvisol_texture_organi .slots = calc_ptf_ferrerjulia2004_gleyic_luvisol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_orthic_luvisol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_orthic_luvisol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1217,6 +1254,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_orthic_luvisol_sand_spec = { .slots = calc_ptf_ferrerjulia2004_orthic_luvisol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_orthic_luvisol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_orthic_luvisol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1268,6 +1307,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_orthic_luvisol_texture_organi .slots = calc_ptf_ferrerjulia2004_orthic_luvisol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_ranker_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_ranker_sand_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -1313,6 +1353,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_ranker_sand_spec = { .slots = calc_ptf_ferrerjulia2004_ranker_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_ranker_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_ranker_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1363,6 +1405,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_ranker_texture_organic_matter .slots = calc_ptf_ferrerjulia2004_ranker_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_calcaric_regosol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_calcaric_regosol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1404,6 +1447,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_calcaric_regosol_sand_spec = .slots = calc_ptf_ferrerjulia2004_calcaric_regosol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_calcaric_regosol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_calcaric_regosol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1457,6 +1502,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_calcaric_regosol_texture_orga .slots = calc_ptf_ferrerjulia2004_calcaric_regosol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_dystric_regosol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_dystric_regosol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1498,6 +1544,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_dystric_regosol_sand_spec = { .slots = calc_ptf_ferrerjulia2004_dystric_regosol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_dystric_regosol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_dystric_regosol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1551,6 +1599,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_dystric_regosol_texture_organ .slots = calc_ptf_ferrerjulia2004_dystric_regosol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_eutric_regosol_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_eutric_regosol_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1594,6 +1643,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_eutric_regosol_sand_spec = { .slots = calc_ptf_ferrerjulia2004_eutric_regosol_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_eutric_regosol_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_eutric_regosol_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1645,6 +1696,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_eutric_regosol_texture_organi .slots = calc_ptf_ferrerjulia2004_eutric_regosol_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_rendzina_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_rendzina_sand_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -1690,6 +1742,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_rendzina_sand_spec = { .slots = calc_ptf_ferrerjulia2004_rendzina_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_rendzina_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_rendzina_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1740,6 +1794,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_rendzina_texture_organic_matt .slots = calc_ptf_ferrerjulia2004_rendzina_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_gleyic_solonchak_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_gleyic_solonchak_sand_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1781,6 +1836,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_gleyic_solonchak_sand_spec = .slots = calc_ptf_ferrerjulia2004_gleyic_solonchak_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_gleyic_solonchak_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_gleyic_solonchak_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1834,6 +1891,7 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_gleyic_solonchak_texture_orga .slots = calc_ptf_ferrerjulia2004_gleyic_solonchak_texture_organic_matter_slots, }; +static const int calc_ptf_ferrerjulia2004_general_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_general_sand_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -1879,6 +1937,8 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_general_sand_spec = { .slots = calc_ptf_ferrerjulia2004_general_sand_slots, }; +static const int calc_ptf_ferrerjulia2004_general_texture_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_ferrerjulia2004_general_texture_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -1930,142 +1990,177 @@ static PyArrayMethod_Spec calc_ptf_ferrerjulia2004_general_texture_organic_matte }; int ptfkit_register_ferrerjulia2004(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_campbell_shiozawa", 2, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_campbell_shiozawa", + calc_ptf_ferrerjulia2004_campbell_shiozawa_types, 2, 1, &calc_ptf_ferrerjulia2004_campbell_shiozawa_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_saxton", 2, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_saxton", + calc_ptf_ferrerjulia2004_saxton_types, 2, 1, &calc_ptf_ferrerjulia2004_saxton_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_dane_puckett", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_dane_puckett", + calc_ptf_ferrerjulia2004_dane_puckett_types, 1, 1, &calc_ptf_ferrerjulia2004_dane_puckett_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_puckett", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_puckett", + calc_ptf_ferrerjulia2004_puckett_types, 1, 1, &calc_ptf_ferrerjulia2004_puckett_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_cosby", 2, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_cosby", + calc_ptf_ferrerjulia2004_cosby_types, 2, 1, &calc_ptf_ferrerjulia2004_cosby_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_humic_acrisol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_humic_acrisol_sand", + calc_ptf_ferrerjulia2004_humic_acrisol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_humic_acrisol_sand_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_humic_acrisol_texture_organic_matter", 3, - 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_humic_acrisol_texture_organic_matter", + calc_ptf_ferrerjulia2004_humic_acrisol_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_humic_acrisol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_calcic_cambisol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_calcic_cambisol_sand", + calc_ptf_ferrerjulia2004_calcic_cambisol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_calcic_cambisol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_calcic_cambisol_texture_organic_matter", - 3, 1, + calc_ptf_ferrerjulia2004_calcic_cambisol_texture_organic_matter_types, 3, + 1, &calc_ptf_ferrerjulia2004_calcic_cambisol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_dystric_cambisol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_dystric_cambisol_sand", + calc_ptf_ferrerjulia2004_dystric_cambisol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_dystric_cambisol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc( - module, "calc_ptf_ferrerjulia2004_dystric_cambisol_texture_organic_matter", 3, 1, + module, "calc_ptf_ferrerjulia2004_dystric_cambisol_texture_organic_matter", + calc_ptf_ferrerjulia2004_dystric_cambisol_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_dystric_cambisol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_eutric_cambisol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_eutric_cambisol_sand", + calc_ptf_ferrerjulia2004_eutric_cambisol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_eutric_cambisol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_eutric_cambisol_texture_organic_matter", - 3, 1, + calc_ptf_ferrerjulia2004_eutric_cambisol_texture_organic_matter_types, 3, + 1, &calc_ptf_ferrerjulia2004_eutric_cambisol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_gleyic_cambisol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_gleyic_cambisol_sand", + calc_ptf_ferrerjulia2004_gleyic_cambisol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_gleyic_cambisol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_gleyic_cambisol_texture_organic_matter", - 3, 1, + calc_ptf_ferrerjulia2004_gleyic_cambisol_texture_organic_matter_types, 3, + 1, &calc_ptf_ferrerjulia2004_gleyic_cambisol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_humic_cambisol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_humic_cambisol_sand", + calc_ptf_ferrerjulia2004_humic_cambisol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_humic_cambisol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_humic_cambisol_texture_organic_matter", - 3, 1, + calc_ptf_ferrerjulia2004_humic_cambisol_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_humic_cambisol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_calcaric_fluvisol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_calcaric_fluvisol_sand", + calc_ptf_ferrerjulia2004_calcaric_fluvisol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_calcaric_fluvisol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc( - module, "calc_ptf_ferrerjulia2004_calcaric_fluvisol_texture_organic_matter", 3, 1, + module, "calc_ptf_ferrerjulia2004_calcaric_fluvisol_texture_organic_matter", + calc_ptf_ferrerjulia2004_calcaric_fluvisol_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_calcaric_fluvisol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_calcic_luvisol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_calcic_luvisol_sand", + calc_ptf_ferrerjulia2004_calcic_luvisol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_calcic_luvisol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_calcic_luvisol_texture_organic_matter", - 3, 1, + calc_ptf_ferrerjulia2004_calcic_luvisol_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_calcic_luvisol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_chromic_luvisol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_chromic_luvisol_sand", + calc_ptf_ferrerjulia2004_chromic_luvisol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_chromic_luvisol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_chromic_luvisol_texture_organic_matter", - 3, 1, + calc_ptf_ferrerjulia2004_chromic_luvisol_texture_organic_matter_types, 3, + 1, &calc_ptf_ferrerjulia2004_chromic_luvisol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_gleyic_luvisol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_gleyic_luvisol_sand", + calc_ptf_ferrerjulia2004_gleyic_luvisol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_gleyic_luvisol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_gleyic_luvisol_texture_organic_matter", - 3, 1, + calc_ptf_ferrerjulia2004_gleyic_luvisol_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_gleyic_luvisol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_orthic_luvisol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_orthic_luvisol_sand", + calc_ptf_ferrerjulia2004_orthic_luvisol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_orthic_luvisol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_orthic_luvisol_texture_organic_matter", - 3, 1, + calc_ptf_ferrerjulia2004_orthic_luvisol_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_orthic_luvisol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_ranker_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_ranker_sand", + calc_ptf_ferrerjulia2004_ranker_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_ranker_sand_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_ranker_texture_organic_matter", 3, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_ranker_texture_organic_matter", + calc_ptf_ferrerjulia2004_ranker_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_ranker_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_calcaric_regosol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_calcaric_regosol_sand", + calc_ptf_ferrerjulia2004_calcaric_regosol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_calcaric_regosol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc( - module, "calc_ptf_ferrerjulia2004_calcaric_regosol_texture_organic_matter", 3, 1, + module, "calc_ptf_ferrerjulia2004_calcaric_regosol_texture_organic_matter", + calc_ptf_ferrerjulia2004_calcaric_regosol_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_calcaric_regosol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_dystric_regosol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_dystric_regosol_sand", + calc_ptf_ferrerjulia2004_dystric_regosol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_dystric_regosol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_dystric_regosol_texture_organic_matter", - 3, 1, + calc_ptf_ferrerjulia2004_dystric_regosol_texture_organic_matter_types, 3, + 1, &calc_ptf_ferrerjulia2004_dystric_regosol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_eutric_regosol_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_eutric_regosol_sand", + calc_ptf_ferrerjulia2004_eutric_regosol_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_eutric_regosol_sand_spec) < 0) return -1; if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_eutric_regosol_texture_organic_matter", - 3, 1, + calc_ptf_ferrerjulia2004_eutric_regosol_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_eutric_regosol_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_rendzina_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_rendzina_sand", + calc_ptf_ferrerjulia2004_rendzina_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_rendzina_sand_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_rendzina_texture_organic_matter", 3, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_rendzina_texture_organic_matter", + calc_ptf_ferrerjulia2004_rendzina_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_rendzina_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_gleyic_solonchak_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_gleyic_solonchak_sand", + calc_ptf_ferrerjulia2004_gleyic_solonchak_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_gleyic_solonchak_sand_spec) < 0) return -1; if (ptfkit_add_ufunc( - module, "calc_ptf_ferrerjulia2004_gleyic_solonchak_texture_organic_matter", 3, 1, + module, "calc_ptf_ferrerjulia2004_gleyic_solonchak_texture_organic_matter", + calc_ptf_ferrerjulia2004_gleyic_solonchak_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_gleyic_solonchak_texture_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_general_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_general_sand", + calc_ptf_ferrerjulia2004_general_sand_types, 1, 1, &calc_ptf_ferrerjulia2004_general_sand_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_general_texture_organic_matter", 3, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_ferrerjulia2004_general_texture_organic_matter", + calc_ptf_ferrerjulia2004_general_texture_organic_matter_types, 3, 1, &calc_ptf_ferrerjulia2004_general_texture_organic_matter_spec) < 0) return -1; return 0; diff --git a/targets/ptfkit-py/src/ptfkit/hodnett2002.c b/targets/ptfkit-py/src/ptfkit/hodnett2002.c index 08134f0..5aee119 100644 --- a/targets/ptfkit-py/src/ptfkit/hodnett2002.c +++ b/targets/ptfkit-py/src/ptfkit/hodnett2002.c @@ -2,6 +2,9 @@ #include #include "ufunc.h" +static const int calc_ptf_hodnett2002_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_hodnett2002_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -73,7 +76,8 @@ static PyArrayMethod_Spec calc_ptf_hodnett2002_spec = { }; int ptfkit_register_hodnett2002(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_hodnett2002", 7, 4, &calc_ptf_hodnett2002_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_hodnett2002", calc_ptf_hodnett2002_types, 7, 4, + &calc_ptf_hodnett2002_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/jabro1992.c b/targets/ptfkit-py/src/ptfkit/jabro1992.c index 6d0bfbc..0361710 100644 --- a/targets/ptfkit-py/src/ptfkit/jabro1992.c +++ b/targets/ptfkit-py/src/ptfkit/jabro1992.c @@ -2,6 +2,7 @@ #include #include "ufunc.h" +static const int calc_ptf_jabro1992_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_jabro1992_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -50,7 +51,8 @@ static PyArrayMethod_Spec calc_ptf_jabro1992_spec = { }; int ptfkit_register_jabro1992(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_jabro1992", 3, 1, &calc_ptf_jabro1992_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_jabro1992", calc_ptf_jabro1992_types, 3, 1, + &calc_ptf_jabro1992_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/li2007.c b/targets/ptfkit-py/src/ptfkit/li2007.c index ec5df28..e7b70ab 100644 --- a/targets/ptfkit-py/src/ptfkit/li2007.c +++ b/targets/ptfkit-py/src/ptfkit/li2007.c @@ -2,6 +2,9 @@ #include #include "ufunc.h" +static const int calc_ptf_li2007_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_li2007_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -67,7 +70,8 @@ static PyArrayMethod_Spec calc_ptf_li2007_spec = { }; int ptfkit_register_li2007(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_li2007", 5, 4, &calc_ptf_li2007_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_li2007", calc_ptf_li2007_types, 5, 4, + &calc_ptf_li2007_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/mayr1999.c b/targets/ptfkit-py/src/ptfkit/mayr1999.c index 0f2ef00..726ee26 100644 --- a/targets/ptfkit-py/src/ptfkit/mayr1999.c +++ b/targets/ptfkit-py/src/ptfkit/mayr1999.c @@ -2,6 +2,8 @@ #include #include "ufunc.h" +static const int calc_ptf_mayr1999_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_mayr1999_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -64,7 +66,8 @@ static PyArrayMethod_Spec calc_ptf_mayr1999_spec = { }; int ptfkit_register_mayr1999(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_mayr1999", 5, 3, &calc_ptf_mayr1999_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_mayr1999", calc_ptf_mayr1999_types, 5, 3, + &calc_ptf_mayr1999_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/oosterveld1980.c b/targets/ptfkit-py/src/ptfkit/oosterveld1980.c index 5b40009..2a35315 100644 --- a/targets/ptfkit-py/src/ptfkit/oosterveld1980.c +++ b/targets/ptfkit-py/src/ptfkit/oosterveld1980.c @@ -2,6 +2,7 @@ #include #include "ufunc.h" +static const int calc_ptf_oosterveld1980_field_capacity_tension_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_oosterveld1980_field_capacity_tension_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -43,6 +44,8 @@ static PyArrayMethod_Spec calc_ptf_oosterveld1980_field_capacity_tension_spec = .slots = calc_ptf_oosterveld1980_field_capacity_tension_slots, }; +static const int calc_ptf_oosterveld1980_retention_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_oosterveld1980_retention_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -99,6 +102,8 @@ static PyArrayMethod_Spec calc_ptf_oosterveld1980_retention_spec = { .slots = calc_ptf_oosterveld1980_retention_slots, }; +static const int calc_ptf_oosterveld1980_field_capacity_types[] = {NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_oosterveld1980_field_capacity_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -150,6 +155,8 @@ static PyArrayMethod_Spec calc_ptf_oosterveld1980_field_capacity_spec = { .slots = calc_ptf_oosterveld1980_field_capacity_slots, }; +static const int calc_ptf_oosterveld1980_wilting_point_types[] = {NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_oosterveld1980_wilting_point_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -201,6 +208,8 @@ static PyArrayMethod_Spec calc_ptf_oosterveld1980_wilting_point_spec = { .slots = calc_ptf_oosterveld1980_wilting_point_slots, }; +static const int calc_ptf_oosterveld1980_available_water_types[] = {NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_oosterveld1980_available_water_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -255,19 +264,24 @@ static PyArrayMethod_Spec calc_ptf_oosterveld1980_available_water_spec = { }; int ptfkit_register_oosterveld1980(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_oosterveld1980_field_capacity_tension", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_oosterveld1980_field_capacity_tension", + calc_ptf_oosterveld1980_field_capacity_tension_types, 1, 1, &calc_ptf_oosterveld1980_field_capacity_tension_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_oosterveld1980_retention", 4, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_oosterveld1980_retention", + calc_ptf_oosterveld1980_retention_types, 4, 1, &calc_ptf_oosterveld1980_retention_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_oosterveld1980_field_capacity", 3, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_oosterveld1980_field_capacity", + calc_ptf_oosterveld1980_field_capacity_types, 3, 1, &calc_ptf_oosterveld1980_field_capacity_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_oosterveld1980_wilting_point", 3, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_oosterveld1980_wilting_point", + calc_ptf_oosterveld1980_wilting_point_types, 3, 1, &calc_ptf_oosterveld1980_wilting_point_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_oosterveld1980_available_water", 3, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_oosterveld1980_available_water", + calc_ptf_oosterveld1980_available_water_types, 3, 1, &calc_ptf_oosterveld1980_available_water_spec) < 0) return -1; return 0; diff --git a/targets/ptfkit-py/src/ptfkit/pidgeon1972.c b/targets/ptfkit-py/src/ptfkit/pidgeon1972.c index 1da5ed8..d451788 100644 --- a/targets/ptfkit-py/src/ptfkit/pidgeon1972.c +++ b/targets/ptfkit-py/src/ptfkit/pidgeon1972.c @@ -2,6 +2,7 @@ #include #include "ufunc.h" +static const int calc_ptf_pidgeon1972_fc_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_pidgeon1972_fc_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -50,6 +51,7 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_fc_spec = { .slots = calc_ptf_pidgeon1972_fc_slots, }; +static const int calc_ptf_pidgeon1972_fc_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_pidgeon1972_fc_sand_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -94,6 +96,8 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_fc_sand_spec = { .slots = calc_ptf_pidgeon1972_fc_sand_slots, }; +static const int calc_ptf_pidgeon1972_fc_sand_organic_matter_types[] = {NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_pidgeon1972_fc_sand_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -142,6 +146,8 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_fc_sand_organic_matter_spec = { .slots = calc_ptf_pidgeon1972_fc_sand_organic_matter_slots, }; +static const int calc_ptf_pidgeon1972_fc_vol_sand_organic_matter_types[] = {NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_pidgeon1972_fc_vol_sand_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -188,6 +194,8 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_fc_vol_sand_organic_matter_spec = .slots = calc_ptf_pidgeon1972_fc_vol_sand_organic_matter_slots, }; +static const int calc_ptf_pidgeon1972_pwp_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_pidgeon1972_pwp_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -237,6 +245,7 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_pwp_spec = { .slots = calc_ptf_pidgeon1972_pwp_slots, }; +static const int calc_ptf_pidgeon1972_pwp_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_pidgeon1972_pwp_sand_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -281,6 +290,8 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_pwp_sand_spec = { .slots = calc_ptf_pidgeon1972_pwp_sand_slots, }; +static const int calc_ptf_pidgeon1972_pwp_sand_organic_matter_types[] = {NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_pidgeon1972_pwp_sand_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -329,6 +340,7 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_pwp_sand_organic_matter_spec = { .slots = calc_ptf_pidgeon1972_pwp_sand_organic_matter_slots, }; +static const int calc_ptf_pidgeon1972_awc_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_pidgeon1972_awc_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -375,6 +387,8 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_awc_spec = { .slots = calc_ptf_pidgeon1972_awc_slots, }; +static const int calc_ptf_pidgeon1972_awc_sand_organic_matter_types[] = {NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_pidgeon1972_awc_sand_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -423,6 +437,7 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_awc_sand_organic_matter_spec = { .slots = calc_ptf_pidgeon1972_awc_sand_organic_matter_slots, }; +static const int calc_ptf_pidgeon1972_awc_coarse_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_pidgeon1972_awc_coarse_sand_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -468,6 +483,7 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_awc_coarse_sand_spec = { .slots = calc_ptf_pidgeon1972_awc_coarse_sand_slots, }; +static const int calc_ptf_pidgeon1972_awc_fine_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_pidgeon1972_awc_fine_sand_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -513,6 +529,7 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_awc_fine_sand_spec = { .slots = calc_ptf_pidgeon1972_awc_fine_sand_slots, }; +static const int calc_ptf_pidgeon1972_awc_very_fine_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_pidgeon1972_awc_very_fine_sand_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -558,6 +575,8 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_awc_very_fine_sand_spec = { .slots = calc_ptf_pidgeon1972_awc_very_fine_sand_slots, }; +static const int calc_ptf_pidgeon1972_eawc_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_pidgeon1972_eawc_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -607,6 +626,7 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_eawc_spec = { .slots = calc_ptf_pidgeon1972_eawc_slots, }; +static const int calc_ptf_pidgeon1972_eawc_sand_types[] = {NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_pidgeon1972_eawc_sand_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -652,6 +672,8 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_eawc_sand_spec = { .slots = calc_ptf_pidgeon1972_eawc_sand_slots, }; +static const int calc_ptf_pidgeon1972_eawc_sand_organic_matter_types[] = {NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_pidgeon1972_eawc_sand_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -698,6 +720,8 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_eawc_sand_organic_matter_spec = { .slots = calc_ptf_pidgeon1972_eawc_sand_organic_matter_slots, }; +static const int calc_ptf_pidgeon1972_eawc_coarse_sand_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_pidgeon1972_eawc_coarse_sand_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -745,6 +769,8 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_eawc_coarse_sand_organic_matter_s .slots = calc_ptf_pidgeon1972_eawc_coarse_sand_organic_matter_slots, }; +static const int calc_ptf_pidgeon1972_eawc_fine_sand_organic_matter_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_pidgeon1972_eawc_fine_sand_organic_matter_contiguous_loop( PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -792,55 +818,67 @@ static PyArrayMethod_Spec calc_ptf_pidgeon1972_eawc_fine_sand_organic_matter_spe }; int ptfkit_register_pidgeon1972(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_fc", 3, 1, &calc_ptf_pidgeon1972_fc_spec) < - 0) + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_fc", calc_ptf_pidgeon1972_fc_types, 3, 1, + &calc_ptf_pidgeon1972_fc_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_fc_sand", 1, 1, - &calc_ptf_pidgeon1972_fc_sand_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_fc_sand", calc_ptf_pidgeon1972_fc_sand_types, + 1, 1, &calc_ptf_pidgeon1972_fc_sand_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_fc_sand_organic_matter", 2, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_fc_sand_organic_matter", + calc_ptf_pidgeon1972_fc_sand_organic_matter_types, 2, 1, &calc_ptf_pidgeon1972_fc_sand_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_fc_vol_sand_organic_matter", 2, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_fc_vol_sand_organic_matter", + calc_ptf_pidgeon1972_fc_vol_sand_organic_matter_types, 2, 1, &calc_ptf_pidgeon1972_fc_vol_sand_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_pwp", 3, 1, &calc_ptf_pidgeon1972_pwp_spec) < - 0) + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_pwp", calc_ptf_pidgeon1972_pwp_types, 3, 1, + &calc_ptf_pidgeon1972_pwp_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_pwp_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_pwp_sand", + calc_ptf_pidgeon1972_pwp_sand_types, 1, 1, &calc_ptf_pidgeon1972_pwp_sand_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_pwp_sand_organic_matter", 2, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_pwp_sand_organic_matter", + calc_ptf_pidgeon1972_pwp_sand_organic_matter_types, 2, 1, &calc_ptf_pidgeon1972_pwp_sand_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_awc", 2, 1, &calc_ptf_pidgeon1972_awc_spec) < - 0) + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_awc", calc_ptf_pidgeon1972_awc_types, 2, 1, + &calc_ptf_pidgeon1972_awc_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_awc_sand_organic_matter", 2, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_awc_sand_organic_matter", + calc_ptf_pidgeon1972_awc_sand_organic_matter_types, 2, 1, &calc_ptf_pidgeon1972_awc_sand_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_awc_coarse_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_awc_coarse_sand", + calc_ptf_pidgeon1972_awc_coarse_sand_types, 1, 1, &calc_ptf_pidgeon1972_awc_coarse_sand_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_awc_fine_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_awc_fine_sand", + calc_ptf_pidgeon1972_awc_fine_sand_types, 1, 1, &calc_ptf_pidgeon1972_awc_fine_sand_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_awc_very_fine_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_awc_very_fine_sand", + calc_ptf_pidgeon1972_awc_very_fine_sand_types, 1, 1, &calc_ptf_pidgeon1972_awc_very_fine_sand_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_eawc", 3, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_eawc", calc_ptf_pidgeon1972_eawc_types, 3, 1, &calc_ptf_pidgeon1972_eawc_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_eawc_sand", 1, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_eawc_sand", + calc_ptf_pidgeon1972_eawc_sand_types, 1, 1, &calc_ptf_pidgeon1972_eawc_sand_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_eawc_sand_organic_matter", 2, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_eawc_sand_organic_matter", + calc_ptf_pidgeon1972_eawc_sand_organic_matter_types, 2, 1, &calc_ptf_pidgeon1972_eawc_sand_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_eawc_coarse_sand_organic_matter", 2, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_eawc_coarse_sand_organic_matter", + calc_ptf_pidgeon1972_eawc_coarse_sand_organic_matter_types, 2, 1, &calc_ptf_pidgeon1972_eawc_coarse_sand_organic_matter_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_eawc_fine_sand_organic_matter", 2, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_pidgeon1972_eawc_fine_sand_organic_matter", + calc_ptf_pidgeon1972_eawc_fine_sand_organic_matter_types, 2, 1, &calc_ptf_pidgeon1972_eawc_fine_sand_organic_matter_spec) < 0) return -1; return 0; diff --git a/targets/ptfkit-py/src/ptfkit/puckett1985.c b/targets/ptfkit-py/src/ptfkit/puckett1985.c index 91b42b7..545782a 100644 --- a/targets/ptfkit-py/src/ptfkit/puckett1985.c +++ b/targets/ptfkit-py/src/ptfkit/puckett1985.c @@ -2,6 +2,9 @@ #include #include "ufunc.h" +static const int calc_ptf_puckett1985_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_puckett1985_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -88,7 +91,8 @@ static PyArrayMethod_Spec calc_ptf_puckett1985_spec = { }; int ptfkit_register_puckett1985(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_puckett1985", 5, 11, &calc_ptf_puckett1985_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_puckett1985", calc_ptf_puckett1985_types, 5, 11, + &calc_ptf_puckett1985_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/rawls1982.c b/targets/ptfkit-py/src/ptfkit/rawls1982.c index 7553ad1..f71965d 100644 --- a/targets/ptfkit-py/src/ptfkit/rawls1982.c +++ b/targets/ptfkit-py/src/ptfkit/rawls1982.c @@ -2,6 +2,7 @@ #include #include "ufunc.h" +static const int calc_ptf_rawls1982_theta_1500_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_rawls1982_theta_1500_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -49,6 +50,8 @@ static PyArrayMethod_Spec calc_ptf_rawls1982_theta_1500_spec = { .slots = calc_ptf_rawls1982_theta_1500_slots, }; +static const int calc_ptf_rawls1982_theta_33_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_rawls1982_theta_33_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -99,6 +102,10 @@ static PyArrayMethod_Spec calc_ptf_rawls1982_theta_33_spec = { .slots = calc_ptf_rawls1982_theta_33_slots, }; +static const int calc_ptf_rawls1982_full_wrc_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_rawls1982_full_wrc_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -191,14 +198,15 @@ static PyArrayMethod_Spec calc_ptf_rawls1982_full_wrc_spec = { }; int ptfkit_register_rawls1982(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_rawls1982_theta_1500", 2, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_rawls1982_theta_1500", + calc_ptf_rawls1982_theta_1500_types, 2, 1, &calc_ptf_rawls1982_theta_1500_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_rawls1982_theta_33", 3, 1, - &calc_ptf_rawls1982_theta_33_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_rawls1982_theta_33", calc_ptf_rawls1982_theta_33_types, + 3, 1, &calc_ptf_rawls1982_theta_33_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_rawls1982_full_wrc", 5, 12, - &calc_ptf_rawls1982_full_wrc_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_rawls1982_full_wrc", calc_ptf_rawls1982_full_wrc_types, + 5, 12, &calc_ptf_rawls1982_full_wrc_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/saxton2006.c b/targets/ptfkit-py/src/ptfkit/saxton2006.c index 52a9415..f553bbb 100644 --- a/targets/ptfkit-py/src/ptfkit/saxton2006.c +++ b/targets/ptfkit-py/src/ptfkit/saxton2006.c @@ -2,6 +2,9 @@ #include #include "ufunc.h" +static const int calc_ptf_saxton2006_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_saxton2006_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -76,6 +79,8 @@ static PyArrayMethod_Spec calc_ptf_saxton2006_spec = { .slots = calc_ptf_saxton2006_slots, }; +static const int calc_ptf_saxton2006_density_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_saxton2006_density_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -140,6 +145,8 @@ static PyArrayMethod_Spec calc_ptf_saxton2006_density_spec = { .slots = calc_ptf_saxton2006_density_slots, }; +static const int calc_ptf_saxton2006_tension_dry_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE}; static int calc_ptf_saxton2006_tension_dry_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -191,6 +198,8 @@ static PyArrayMethod_Spec calc_ptf_saxton2006_tension_dry_spec = { .slots = calc_ptf_saxton2006_tension_dry_slots, }; +static const int calc_ptf_saxton2006_tension_wet_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_saxton2006_tension_wet_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -247,6 +256,8 @@ static PyArrayMethod_Spec calc_ptf_saxton2006_tension_wet_spec = { .slots = calc_ptf_saxton2006_tension_wet_slots, }; +static const int calc_ptf_saxton2006_conductivity_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_saxton2006_conductivity_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -303,6 +314,8 @@ static PyArrayMethod_Spec calc_ptf_saxton2006_conductivity_spec = { .slots = calc_ptf_saxton2006_conductivity_slots, }; +static const int calc_ptf_saxton2006_gravel_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_saxton2006_gravel_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -366,6 +379,8 @@ static PyArrayMethod_Spec calc_ptf_saxton2006_gravel_spec = { .slots = calc_ptf_saxton2006_gravel_slots, }; +static const int calc_ptf_saxton2006_salinity_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_saxton2006_salinity_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -422,25 +437,29 @@ static PyArrayMethod_Spec calc_ptf_saxton2006_salinity_spec = { }; int ptfkit_register_saxton2006(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006", 3, 10, &calc_ptf_saxton2006_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006", calc_ptf_saxton2006_types, 3, 10, + &calc_ptf_saxton2006_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_density", 4, 4, - &calc_ptf_saxton2006_density_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_density", calc_ptf_saxton2006_density_types, + 4, 4, &calc_ptf_saxton2006_density_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_tension_dry", 3, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_tension_dry", + calc_ptf_saxton2006_tension_dry_types, 3, 1, &calc_ptf_saxton2006_tension_dry_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_tension_wet", 4, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_tension_wet", + calc_ptf_saxton2006_tension_wet_types, 4, 1, &calc_ptf_saxton2006_tension_wet_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_conductivity", 4, 1, + if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_conductivity", + calc_ptf_saxton2006_conductivity_types, 4, 1, &calc_ptf_saxton2006_conductivity_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_gravel", 4, 4, - &calc_ptf_saxton2006_gravel_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_gravel", calc_ptf_saxton2006_gravel_types, 4, + 4, &calc_ptf_saxton2006_gravel_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_salinity", 3, 2, - &calc_ptf_saxton2006_salinity_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_saxton2006_salinity", calc_ptf_saxton2006_salinity_types, + 3, 2, &calc_ptf_saxton2006_salinity_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/tiwary2014.c b/targets/ptfkit-py/src/ptfkit/tiwary2014.c index a39603f..0ca6738 100644 --- a/targets/ptfkit-py/src/ptfkit/tiwary2014.c +++ b/targets/ptfkit-py/src/ptfkit/tiwary2014.c @@ -2,6 +2,7 @@ #include #include "ufunc.h" +static const int calc_ptf_tiwary2014_igp_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_tiwary2014_igp_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -50,6 +51,9 @@ static PyArrayMethod_Spec calc_ptf_tiwary2014_igp_spec = { .slots = calc_ptf_tiwary2014_igp_slots, }; +static const int calc_ptf_tiwary2014_bsr_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_tiwary2014_bsr_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -119,11 +123,11 @@ static PyArrayMethod_Spec calc_ptf_tiwary2014_bsr_spec = { }; int ptfkit_register_tiwary2014(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_tiwary2014_igp", 3, 1, &calc_ptf_tiwary2014_igp_spec) < - 0) + if (ptfkit_add_ufunc(module, "calc_ptf_tiwary2014_igp", calc_ptf_tiwary2014_igp_types, 3, 1, + &calc_ptf_tiwary2014_igp_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_tiwary2014_bsr", 6, 4, &calc_ptf_tiwary2014_bsr_spec) < - 0) + if (ptfkit_add_ufunc(module, "calc_ptf_tiwary2014_bsr", calc_ptf_tiwary2014_bsr_types, 6, 4, + &calc_ptf_tiwary2014_bsr_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/ufunc.h b/targets/ptfkit-py/src/ptfkit/ufunc.h index b694d7c..0f91589 100644 --- a/targets/ptfkit-py/src/ptfkit/ufunc.h +++ b/targets/ptfkit-py/src/ptfkit/ufunc.h @@ -7,11 +7,13 @@ static inline double ptfkit_pow4(double value) { return square * square; } -static inline int ptfkit_add_ufunc(PyObject *module, const char *name, int nin, int nout, - PyArrayMethod_Spec *spec) { +static inline int ptfkit_add_ufunc(PyObject *module, const char *name, const int *types, int nin, + int nout, PyArrayMethod_Spec *spec) { PyArray_DTypeMeta *dtypes[NPY_MAXARGS]; - for (int argument = 0; argument < nin + nout; argument++) - dtypes[argument] = &PyArray_DoubleDType; + for (int argument = 0; argument < nin + nout; argument++) { + dtypes[argument] = + types[argument] == NPY_UINT32 ? &PyArray_UInt32DType : &PyArray_DoubleDType; + } spec->dtypes = dtypes; PyObject *ufunc = PyUFunc_FromFuncAndData(NULL, NULL, NULL, 0, nin, nout, PyUFunc_None, name, NULL, 0); diff --git a/targets/ptfkit-py/src/ptfkit/varallyai1982.c b/targets/ptfkit-py/src/ptfkit/varallyai1982.c index 3e6b8ea..c0cd6f8 100644 --- a/targets/ptfkit-py/src/ptfkit/varallyai1982.c +++ b/targets/ptfkit-py/src/ptfkit/varallyai1982.c @@ -2,6 +2,8 @@ #include #include "ufunc.h" +static const int calc_ptf_varallyai1982_meadow_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_varallyai1982_meadow_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -60,6 +62,8 @@ static PyArrayMethod_Spec calc_ptf_varallyai1982_meadow_spec = { .slots = calc_ptf_varallyai1982_meadow_slots, }; +static const int calc_ptf_varallyai1982_chernozem_a_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_varallyai1982_chernozem_a_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -116,6 +120,8 @@ static PyArrayMethod_Spec calc_ptf_varallyai1982_chernozem_a_spec = { .slots = calc_ptf_varallyai1982_chernozem_a_slots, }; +static const int calc_ptf_varallyai1982_chernozem_b_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_varallyai1982_chernozem_b_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -172,6 +178,8 @@ static PyArrayMethod_Spec calc_ptf_varallyai1982_chernozem_b_spec = { .slots = calc_ptf_varallyai1982_chernozem_b_slots, }; +static const int calc_ptf_varallyai1982_chernozem_c_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_varallyai1982_chernozem_c_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -229,16 +237,20 @@ static PyArrayMethod_Spec calc_ptf_varallyai1982_chernozem_c_spec = { }; int ptfkit_register_varallyai1982(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_varallyai1982_meadow", 3, 3, + if (ptfkit_add_ufunc(module, "calc_ptf_varallyai1982_meadow", + calc_ptf_varallyai1982_meadow_types, 3, 3, &calc_ptf_varallyai1982_meadow_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_varallyai1982_chernozem_a", 2, 3, + if (ptfkit_add_ufunc(module, "calc_ptf_varallyai1982_chernozem_a", + calc_ptf_varallyai1982_chernozem_a_types, 2, 3, &calc_ptf_varallyai1982_chernozem_a_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_varallyai1982_chernozem_b", 2, 3, + if (ptfkit_add_ufunc(module, "calc_ptf_varallyai1982_chernozem_b", + calc_ptf_varallyai1982_chernozem_b_types, 2, 3, &calc_ptf_varallyai1982_chernozem_b_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_varallyai1982_chernozem_c", 2, 3, + if (ptfkit_add_ufunc(module, "calc_ptf_varallyai1982_chernozem_c", + calc_ptf_varallyai1982_chernozem_c_types, 2, 3, &calc_ptf_varallyai1982_chernozem_c_spec) < 0) return -1; return 0; diff --git a/targets/ptfkit-py/src/ptfkit/vereecken1989.c b/targets/ptfkit-py/src/ptfkit/vereecken1989.c index d531cbf..1ed9297 100644 --- a/targets/ptfkit-py/src/ptfkit/vereecken1989.c +++ b/targets/ptfkit-py/src/ptfkit/vereecken1989.c @@ -2,6 +2,8 @@ #include #include "ufunc.h" +static const int calc_ptf_vereecken1989_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_vereecken1989_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, @@ -64,6 +66,10 @@ static PyArrayMethod_Spec calc_ptf_vereecken1989_spec = { .slots = calc_ptf_vereecken1989_slots, }; +static const int calc_ptf_vereecken1989_detailed_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_vereecken1989_detailed_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, @@ -162,9 +168,11 @@ static PyArrayMethod_Spec calc_ptf_vereecken1989_detailed_spec = { }; int ptfkit_register_vereecken1989(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_vereecken1989", 4, 4, &calc_ptf_vereecken1989_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_vereecken1989", calc_ptf_vereecken1989_types, 4, 4, + &calc_ptf_vereecken1989_spec) < 0) return -1; - if (ptfkit_add_ufunc(module, "calc_ptf_vereecken1989_detailed", 13, 4, + if (ptfkit_add_ufunc(module, "calc_ptf_vereecken1989_detailed", + calc_ptf_vereecken1989_detailed_types, 13, 4, &calc_ptf_vereecken1989_detailed_spec) < 0) return -1; return 0; diff --git a/targets/ptfkit-py/src/ptfkit/wang2012.c b/targets/ptfkit-py/src/ptfkit/wang2012.c index 7b7bd5d..d1cc2ab 100644 --- a/targets/ptfkit-py/src/ptfkit/wang2012.c +++ b/targets/ptfkit-py/src/ptfkit/wang2012.c @@ -2,6 +2,9 @@ #include #include "ufunc.h" +static const int calc_ptf_wang2012_types[] = {NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_wang2012_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -67,7 +70,8 @@ static PyArrayMethod_Spec calc_ptf_wang2012_spec = { }; int ptfkit_register_wang2012(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_wang2012", 6, 3, &calc_ptf_wang2012_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_wang2012", calc_ptf_wang2012_types, 6, 3, + &calc_ptf_wang2012_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/src/ptfkit/weber2020.c b/targets/ptfkit-py/src/ptfkit/weber2020.c index baf5518..03074a8 100644 --- a/targets/ptfkit-py/src/ptfkit/weber2020.c +++ b/targets/ptfkit-py/src/ptfkit/weber2020.c @@ -2,6 +2,9 @@ #include #include "ufunc.h" +static const int calc_ptf_weber2020_types[] = { + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; static int calc_ptf_weber2020_contiguous_loop(PyArrayMethod_Context *context, char *const *data, const npy_intp *dimensions, const npy_intp *strides, NpyAuxData *transferdata) { @@ -79,7 +82,8 @@ static PyArrayMethod_Spec calc_ptf_weber2020_spec = { }; int ptfkit_register_weber2020(PyObject *module) { - if (ptfkit_add_ufunc(module, "calc_ptf_weber2020", 6, 7, &calc_ptf_weber2020_spec) < 0) + if (ptfkit_add_ufunc(module, "calc_ptf_weber2020", calc_ptf_weber2020_types, 6, 7, + &calc_ptf_weber2020_spec) < 0) return -1; return 0; } diff --git a/targets/ptfkit-py/tests/_helpers.py b/targets/ptfkit-py/tests/_helpers.py index f0578b2..a45ebb9 100644 --- a/targets/ptfkit-py/tests/_helpers.py +++ b/targets/ptfkit-py/tests/_helpers.py @@ -1,18 +1,22 @@ from __future__ import annotations +from enum import Enum from typing import TYPE_CHECKING, NamedTuple, TypeVar, overload import numpy as np +from ptfkit.enums import EnumArray + if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Mapping, Sequence + from typing import Any R = TypeVar('R', bound=NamedTuple) - GoldenCase = tuple[dict[str, float], dict[str, float], float, float] + GoldenCase = tuple[Mapping[str, Any], dict[str, float], float, float] VectorCasePart = tuple[ - dict[str, np.ndarray], + dict[str, Any], dict[str, float], float, float, @@ -34,7 +38,14 @@ def prepare_vector_case( result_cls: type[R] | None = None, ) -> VectorCaseScalar | VectorCaseTuple: inputs, expected, rtol, atol = cases[0] - vector_inputs = {name: np.array([value]) for name, value in inputs.items()} + vector_inputs = { + name: ( + EnumArray._from_members(type(value), [value]) # noqa: SLF001 + if isinstance(value, Enum) + else np.array([value]) + ) + for name, value in inputs.items() + } out: np.ndarray | R if result_cls is None: From 47241eabd46762aa822e7822610b76f7771e2b49 Mon Sep 17 00:00:00 2001 From: Petr Tsymbarovich Date: Thu, 27 Aug 2026 22:43:48 +0300 Subject: [PATCH 2/3] refactor(ai): simplify the generate skill --- .agents/skills/ptf-generate/SKILL.md | 42 +++++++-------- .../references/generation-checklist.md | 54 ------------------- docs/src/contributing/development.md | 12 ++--- 3 files changed, 27 insertions(+), 81 deletions(-) delete mode 100644 .agents/skills/ptf-generate/references/generation-checklist.md diff --git a/.agents/skills/ptf-generate/SKILL.md b/.agents/skills/ptf-generate/SKILL.md index 1c87bf1..82288a4 100644 --- a/.agents/skills/ptf-generate/SKILL.md +++ b/.agents/skills/ptf-generate/SKILL.md @@ -1,6 +1,6 @@ --- name: ptf-generate -description: Generate and verify all retained ptfkit targets for one reviewed APA-style source slug. Use after human review of a YAML source file in specs/functions to validate, generate, test, prove idempotence, and atomically mark the source implemented. +description: Generate and verify all retained ptfkit targets for one reviewed APA-style source slug. Use after human review of a YAML source file in specs/functions to validate, generate, run the complete verification suite, and atomically mark the source implemented. --- # PTF Generate @@ -17,30 +17,30 @@ status when input validation fails; report the blocking input error. ## Procedure -1. Treat the argument as the source under review. Read - `specs/schema/ptf-spec.schema.json`, its selected YAML file, - and `references/generation-checklist.md`. -2. Reject unresolved blockers, `TODO` values, schema or semantic failures, and - output-metadata mismatches. Record `outputs.name` is PascalCase and names - generated structures and classes; `$defs` keys only resolve local references. - For categorical inputs and lookups, verify the named enum binding, exact - member names and canonical values, complete enum-to-record mapping, lookup - key type, row fields, and categorical golden inputs. Do not infer missing - science. -3. Validate, generate all retained targets, and run the required verification - gates. Before validation, extract each repeated nontrivial formula - expression within a function into one earlier local implementation variable - and reference it thereafter; retain published numeric lexemes and do not - invent scientific semantics for the calculation intermediate. A generator - capability gap is a blocker, never an invitation to hand-write that - computational target. -4. After every required check passes, change the selected source functions from - `ready-for-implementation` to `implemented`, then revalidate, regenerate, - and prove the second generation pass is idempotent. +1. Read the selected YAML file and require at least one function with status + `ready-for-implementation`. Only those functions participate in the status + transition; leave functions with any other status unchanged. +2. Run `mise run validate`. Treat any structural or semantic validation failure + as a blocker. +3. Run `mise run generate` to update every codegen-owned target. +4. Run `mise run verify`. Report any generator capability or target failure as + a blocker; do not hand-write a generated computational target. +5. Only after all verification passes, change the selected source functions + from `ready-for-implementation` to `implemented`. +6. Run `mise run generate` once more to produce the final implemented state. + Treat the status update and final generation as one transition: do not leave + the selected functions marked `implemented` if final generation fails. ## Hard rules - Never hand-edit marked generated files. +- Do not rewrite formulas, implementation variables, metadata, or other + scientific content. Human review must complete those changes before this + skill runs. - `generation.public_python: manual` permits only a hand-written public wrapper that delegates to the generated native ufunc. - Do not change status unless all required retained targets pass. +- Do not manually audit generated output or prove regeneration idempotence for + a routine PTF addition. Use those checks when the generator, schema, output + formatting, or generation infrastructure changes, or when explicitly running + `ptf-review`. diff --git a/.agents/skills/ptf-generate/references/generation-checklist.md b/.agents/skills/ptf-generate/references/generation-checklist.md deleted file mode 100644 index 7e1276b..0000000 --- a/.agents/skills/ptf-generate/references/generation-checklist.md +++ /dev/null @@ -1,54 +0,0 @@ -# Generation checklist - -## Preconditions - -- The requested APA-style slug identifies - `specs/functions/.yaml`; its filename stem is the sole - source identity. -- Every selected function is `ready-for-implementation`, has no unresolved - blocker or `TODO`, and has complete schema-valid semantic implementation and - matching ordered output metadata. -- Every record `outputs.name` is PascalCase and names generated structures and - classes. -- Every categorical input binds a function-local `name` to an enum `$ref`. - Enum member names and canonical textual values are unique, and categorical - golden inputs name enum members rather than textual values or ordinals. -- Every lookup references an enum input and record output, covers every enum - member exactly once, and gives every row exactly the record's fields. Each - lookup invocation uses an in-scope key of the declared enum type; field - access and direct record return match the resolved record type. -- Run `cargo run --manifest-path codegen/Cargo.toml -- validate` before - generation. - -## Required commands - -Run the relevant project gates after -`cargo run --manifest-path codegen/Cargo.toml -- generate`: - -```sh -mise run codegen:format -mise run codegen:lint -mise run codegen:test -mise run rust:format -mise run rust:lint -mise run rust:test -mise run python:format -mise run python:lint -mise run python:test -``` - -Run `cargo run --manifest-path codegen/Cargo.toml -- generate` a second time and -inspect the diff for idempotence. Once all gates pass, set only the selected -source's functions to `implemented`, then rerun validation, generation, and the -second generation idempotence check. Investigate every unexpected diff. - -## Failure classification - -- Invalid, incomplete, or ambiguous science: return a spec blocker for the - user to resolve. -- Valid semantic IR unsupported by any retained Rust, C, C++, or Python path: - return a generator capability blocker. This includes categorical types, typed - lookups, and record-field access. Do not hand-write a retained computational - target. -- A manual public module may only wrap its generated native ufunc; it does not - disable native generation or allow a duplicate formula. diff --git a/docs/src/contributing/development.md b/docs/src/contributing/development.md index ef4bec0..cdb5dbf 100644 --- a/docs/src/contributing/development.md +++ b/docs/src/contributing/development.md @@ -82,12 +82,12 @@ reference pages directly. Regenerate every target and the PTF catalog with: mise run generate ``` -A second generation run must leave the working tree unchanged. - -To check this across every codegen-owned target family, run: +When changing the generator, specification schema, output formatting, or +generation infrastructure, verify deterministic regeneration across every +codegen-owned target family with: ```sh -mise run check-generated +mise run generate-check ``` The command regenerates the targets through the normal pipeline and reports @@ -118,8 +118,8 @@ The assisted workflow uses the skills in `.agents/skills/`: review-ready draft under `specs/functions/` using only information supported by that source. 2. Review the YAML and resolve every blocker, including missing metadata. -3. Run `ptf-generate ` to validate, generate, test, prove - idempotence, and mark the reviewed source implemented. +3. Run `ptf-generate ` to validate, generate, run the complete + verification suite, and mark the reviewed source implemented. 4. Optionally run `ptf-review ` for an independent, read-only pre-merge review. From 95843aa010833eb0b2f69d2da40c3f05b0cb8d95 Mon Sep 17 00:00:00 2001 From: "K.Tolstygin" Date: Fri, 28 Aug 2026 14:37:11 +0300 Subject: [PATCH 3/3] feat: add Clapp et al. (1978) --- docs/src/ptf-catalog/sources/clapp1978.md | 63 +++ docs/src/ptf-catalog/sources/index.md | 1 + docs/src/reference/c/functions.md | 1 + docs/src/reference/c/headers/clapp1978.md | 115 ++++++ docs/src/reference/c/headers/ptfkit.md | 1 + docs/src/reference/c/index.md | 1 + docs/src/reference/cpp/functions.md | 1 + docs/src/reference/cpp/index.md | 1 + docs/src/reference/cpp/modules/clapp1978.md | 119 ++++++ docs/src/reference/cpp/modules/ptfkit.md | 1 + docs/src/reference/python/clapp1978.md | 8 + docs/src/reference/python/index.md | 1 + specs/functions/clapp1978.yaml | 268 +++++++++++++ targets/ptfkit-native/cpp/clapp1978.cppm | 168 ++++++++ targets/ptfkit-native/cpp/ptfkit.cppm | 1 + .../ptfkit-native/include/ptfkit/clapp1978.h | 165 ++++++++ targets/ptfkit-native/include/ptfkit/ptfkit.h | 1 + targets/ptfkit-native/tests/c/clapp1978.c | 115 ++++++ targets/ptfkit-native/tests/cpp/clapp1978.cpp | 146 +++++++ targets/ptfkit-py/src/ptfkit/_ptfkit.pyi | 1 + targets/ptfkit-py/src/ptfkit/clapp1978.c | 69 ++++ targets/ptfkit-py/src/ptfkit/clapp1978.py | 171 ++++++++ targets/ptfkit-py/src/ptfkit/ptfkit.c | 5 + targets/ptfkit-py/tests/test_clapp1978.py | 220 +++++++++++ targets/ptfkit-py/tests/test_enum_array.py | 45 +++ targets/ptfkit-rs/src/clapp1978.rs | 368 ++++++++++++++++++ 26 files changed, 2056 insertions(+) create mode 100644 docs/src/ptf-catalog/sources/clapp1978.md create mode 100644 docs/src/reference/c/headers/clapp1978.md create mode 100644 docs/src/reference/cpp/modules/clapp1978.md create mode 100644 docs/src/reference/python/clapp1978.md create mode 100644 specs/functions/clapp1978.yaml create mode 100644 targets/ptfkit-native/cpp/clapp1978.cppm create mode 100644 targets/ptfkit-native/include/ptfkit/clapp1978.h create mode 100644 targets/ptfkit-native/tests/c/clapp1978.c create mode 100644 targets/ptfkit-native/tests/cpp/clapp1978.cpp create mode 100644 targets/ptfkit-py/src/ptfkit/clapp1978.c create mode 100644 targets/ptfkit-py/src/ptfkit/clapp1978.py create mode 100644 targets/ptfkit-py/tests/test_clapp1978.py create mode 100644 targets/ptfkit-py/tests/test_enum_array.py create mode 100644 targets/ptfkit-rs/src/clapp1978.rs diff --git a/docs/src/ptf-catalog/sources/clapp1978.md b/docs/src/ptf-catalog/sources/clapp1978.md new file mode 100644 index 0000000..3ee07c8 --- /dev/null +++ b/docs/src/ptf-catalog/sources/clapp1978.md @@ -0,0 +1,63 @@ +--- +# @generated by ptfkit-codegen; DO NOT EDIT. + +title: PTF source clapp1978 +nav-title: clapp1978 +--- + +# Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. + +## Source + +Clapp, R. B., & Hornberger, G. M. (1978). Empirical equations for some soil hydraulic properties. Water Resources Research. + +## Scope + +**Territory:** United States + +**Dataset:** Desorption data reported by Holtan et al. (1968) for soils collected at 34 United States localities; 1,446 soils remained from an initial set of more than 1,800 after the exclusions described by the paper. + +## Functions + +### `calc_ptf_clapp1978` + +Return representative soil hydraulic parameters for a USDA texture class. + +**Status:** `implemented` + +**Prediction target:** Representative moisture-characteristic exponent, saturation suction, wetting-front suction, saturated water content, saturated hydraulic conductivity, and sorptivity for a soil textural class. + +**Models:** $h(\theta)$ — Power-curve moisture characteristic with a parabolic gradual-air-entry section near saturation; $k(h)$ — Campbell relative-conductivity power relation + +#### Inputs + +| Name | Type | Unit | Domain | Description | +| --- | --- | --- | --- | --- | +| `soil_texture` | `UsdaTextureClass` | — | — | USDA soil textural class used to select a row of Table 2. | + +#### Outputs + +| Name | Unit | Domain | Description | +| --- | --- | --- | --- | +| `b` | 1 | value > 0 | Mean exponent of the moisture-characteristic power curve. | +| `saturation_suction` | cm | value > 0 | Representative saturation suction, calculated as the antilog of the mean logarithm of fitted saturation-suction values. | +| `wetting_front_suction` | cm | value > 0 | Representative Green-Ampt wetting-front suction. | +| `saturated_water_content` | 1 | 0 < value < 1 | Mean saturated volumetric water content, taken as total porosity. | +| `saturated_hydraulic_conductivity` | cm/min | value > 0 | Mean saturated hydraulic conductivity reported by Li et al. (1976). | +| `sorptivity` | cm/min^(1/2) | value > 0 | Representative sorptivity for the paper's single initial moisture deficit corresponding to 500-cm initial suction. | + +!!! note + + The representative wetting-front suctions use an inflection wetness of 0.92 to model gradual air entry near saturation. + +!!! note + + The sorptivity values use the average saturated hydraulic conductivities reported by Li et al. (1976) and one initial moisture deficit at 500-cm suction. + +!!! warning + + The paper states that these unverified average values should not be used blindly because saturation suction varies substantially within texture classes. + +!!! warning + + Sorptivity values must be treated cautiously because the conductivity averages may not represent the same average soils as the other parameters. diff --git a/docs/src/ptf-catalog/sources/index.md b/docs/src/ptf-catalog/sources/index.md index d661714..a0400a6 100644 --- a/docs/src/ptf-catalog/sources/index.md +++ b/docs/src/ptf-catalog/sources/index.md @@ -14,6 +14,7 @@ Each page describes the source, scope, inputs, outputs, status, and limitations | [Aimrun & Amin (2009), Tanjung Karang Rice Irrigation Project, Malaysia.](./aimrun2009.md) | Tanjung Karang Rice Irrigation Project, located on a flat coastal plain in the Integrated Agricultural Development Area (IADA Barat Laut Selangor), Malaysia | 1 | | [Beniaich et al. (2023), soil-water PTFs for four Moroccan regions.](./beniaich2023.md) | Agricultural topsoils in Doukkala, Gharb-Loukouss, Moulouya, and Tadla, Morocco | 14 | | [Chakraborty et al. (2011), point water-retention PTFs for Indian soils.](./chakraborty2011.md) | India | 6 | +| [Clapp and Hornberger (1978) representative soil hydraulic parameters by texture.](./clapp1978.md) | United States | 1 | | [Cosby et al. (1984), United States.](./cosby1984.md) | United States | 1 | | [Dharumarajan et al. (2019) hydraulic PTFs for the Karnataka Plateau.](./dharumarajan2019.md) | Karnataka Plateau, India | 5 | | [Ferrer Julià et al. (2004), saturated-conductivity PTFs for Spanish soils.](./ferrerjulia2004.md) | Spanish mainland on the Iberian Peninsula | 41 | diff --git a/docs/src/reference/c/functions.md b/docs/src/reference/c/functions.md index feea01a..a8ff235 100644 --- a/docs/src/reference/c/functions.md +++ b/docs/src/reference/c/functions.md @@ -27,6 +27,7 @@ title: C function index | [`calc_ptf_chakraborty2011_eq4`](headers/chakraborty2011.md#function-calc_ptf_chakraborty2011_eq4) | Estimate four gravimetric water contents from clay, silt, and sand. | [``](headers/chakraborty2011.md) | | [`calc_ptf_chakraborty2011_eq5`](headers/chakraborty2011.md#function-calc_ptf_chakraborty2011_eq5) | Estimate four gravimetric water contents from clay, silt, sand, and bulk density. | [``](headers/chakraborty2011.md) | | [`calc_ptf_chakraborty2011_eq6`](headers/chakraborty2011.md#function-calc_ptf_chakraborty2011_eq6) | Estimate four gravimetric water contents from texture, organic carbon, and bulk density. | [``](headers/chakraborty2011.md) | +| [`calc_ptf_clapp1978`](headers/clapp1978.md#function-calc_ptf_clapp1978) | Return representative soil hydraulic parameters for a USDA texture class. | [``](headers/clapp1978.md) | | [`calc_ptf_cosby1984_univariate`](headers/cosby1984.md#function-calc_ptf_cosby1984_univariate) | Estimate Cosby et al. (1984) univariate hydraulic parameter statistics from soil texture. | [``](headers/cosby1984.md) | | [`calc_ptf_dharumarajan2019_infiltration`](headers/dharumarajan2019.md#function-calc_ptf_dharumarajan2019_infiltration) | Estimate infiltration rate for Karnataka soils from texture fractions. | [``](headers/dharumarajan2019.md) | | [`calc_ptf_dharumarajan2019_nkp`](headers/dharumarajan2019.md#function-calc_ptf_dharumarajan2019_nkp) | Estimate field capacity and wilting point for Northern Karnataka soils. | [``](headers/dharumarajan2019.md) | diff --git a/docs/src/reference/c/headers/clapp1978.md b/docs/src/reference/c/headers/clapp1978.md new file mode 100644 index 0000000..993a4c4 --- /dev/null +++ b/docs/src/reference/c/headers/clapp1978.md @@ -0,0 +1,115 @@ +--- +title: "clapp1978.h" +--- + + + +# `` + +```c +#include +``` + +Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. + +## Source + +Clapp, R. B., & Hornberger, G. M. (1978). Empirical equations for some soil hydraulic properties. Water Resources Research. + +## Scope + +**Territory:** United States + +**Dataset:** Desorption data reported by Holtan et al. (1968) for soils collected at 34 United States localities; 1,446 soils remained from an initial set of more than 1,800 after the exclusions described by the paper. + +[PTF catalog page](../../../ptf-catalog/sources/clapp1978.md) + +## `clapp1978_usda_texture_class` + +```c +typedef enum { + clapp1978_usda_texture_class_sand, + clapp1978_usda_texture_class_loamy_sand, + clapp1978_usda_texture_class_sandy_loam, + clapp1978_usda_texture_class_silt_loam, + clapp1978_usda_texture_class_loam, + clapp1978_usda_texture_class_sandy_clay_loam, + clapp1978_usda_texture_class_silty_clay_loam, + clapp1978_usda_texture_class_clay_loam, + clapp1978_usda_texture_class_sandy_clay, + clapp1978_usda_texture_class_silty_clay, + clapp1978_usda_texture_class_clay, +} clapp1978_usda_texture_class; +``` + +| Member | Canonical value | Description | +| --- | --- | --- | +| `clapp1978_usda_texture_class_sand` | `sand` | USDA sand soil textural class. | +| `clapp1978_usda_texture_class_loamy_sand` | `loamy sand` | USDA loamy sand soil textural class. | +| `clapp1978_usda_texture_class_sandy_loam` | `sandy loam` | USDA sandy loam soil textural class. | +| `clapp1978_usda_texture_class_silt_loam` | `silt loam` | USDA silt loam soil textural class. | +| `clapp1978_usda_texture_class_loam` | `loam` | USDA loam soil textural class. | +| `clapp1978_usda_texture_class_sandy_clay_loam` | `sandy clay loam` | USDA sandy clay loam soil textural class. | +| `clapp1978_usda_texture_class_silty_clay_loam` | `silty clay loam` | USDA silty clay loam soil textural class. | +| `clapp1978_usda_texture_class_clay_loam` | `clay loam` | USDA clay loam soil textural class. | +| `clapp1978_usda_texture_class_sandy_clay` | `sandy clay` | USDA sandy clay soil textural class. | +| `clapp1978_usda_texture_class_silty_clay` | `silty clay` | USDA silty clay soil textural class. | +| `clapp1978_usda_texture_class_clay` | `clay` | USDA clay soil textural class. | + +## `clapp1978_parameters` + +```c +typedef struct { + double b; + double saturation_suction; + double wetting_front_suction; + double saturated_water_content; + double saturated_hydraulic_conductivity; + double sorptivity; + } clapp1978_parameters; +``` + +| Field | Description | +| --- | --- | +| `b` | Mean exponent of the moisture-characteristic power curve. (1) | +| `saturation_suction` | Representative saturation suction, calculated as the antilog of the mean logarithm of fitted saturation-suction values. (cm) | +| `wetting_front_suction` | Representative Green-Ampt wetting-front suction. (cm) | +| `saturated_water_content` | Mean saturated volumetric water content, taken as total porosity. (1) | +| `saturated_hydraulic_conductivity` | Mean saturated hydraulic conductivity reported by Li et al. (1976). (cm/min) | +| `sorptivity` | Representative sorptivity for the paper's single initial moisture deficit corresponding to 500-cm initial suction. (cm/min^(1/2)) | + +## Functions + +### `calc_ptf_clapp1978` {#function-calc_ptf_clapp1978} + +Return representative soil hydraulic parameters for a USDA texture class. + +```c +static inline clapp1978_parameters calc_ptf_clapp1978(clapp1978_usda_texture_class soil_texture); +``` + +#### Parameters + +| Name | Direction | Description | +| --- | --- | --- | +| `soil_texture` | in | USDA soil textural class used to select a row of Table 2. | + +#### Returns + +A `clapp1978_parameters` value. + +!!! note + + The representative wetting-front suctions use an inflection wetness of 0.92 to model gradual air entry near saturation. + +!!! note + + The sorptivity values use the average saturated hydraulic conductivities reported by Li et al. (1976) and one initial moisture deficit at 500-cm suction. + +!!! warning + + The paper states that these unverified average values should not be used blindly because saturation suction varies substantially within texture classes. + +!!! warning + + Sorptivity values must be treated cautiously because the conductivity averages may not represent the same average soils as the other parameters. diff --git a/docs/src/reference/c/headers/ptfkit.md b/docs/src/reference/c/headers/ptfkit.md index 099a16c..66a7827 100644 --- a/docs/src/reference/c/headers/ptfkit.md +++ b/docs/src/reference/c/headers/ptfkit.md @@ -18,6 +18,7 @@ This umbrella header aggregates every public ptfkit source header. Include an in - [``](aimrun2009.md) — Aimrun & Amin (2009), Tanjung Karang Rice Irrigation Project, Malaysia. - [``](beniaich2023.md) — Beniaich et al. (2023), soil-water PTFs for four Moroccan regions. - [``](chakraborty2011.md) — Chakraborty et al. (2011), point water-retention PTFs for Indian soils. +- [``](clapp1978.md) — Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. - [``](cosby1984.md) — Cosby et al. (1984), United States. - [``](dharumarajan2019.md) — Dharumarajan et al. (2019) hydraulic PTFs for the Karnataka Plateau. - [``](ferrerjulia2004.md) — Ferrer Julià et al. (2004), saturated-conductivity PTFs for Spanish soils. diff --git a/docs/src/reference/c/index.md b/docs/src/reference/c/index.md index 477636e..376c8f8 100644 --- a/docs/src/reference/c/index.md +++ b/docs/src/reference/c/index.md @@ -15,6 +15,7 @@ ptfkit's C API is organized around installed headers. - [``](headers/aimrun2009.md) — Aimrun & Amin (2009), Tanjung Karang Rice Irrigation Project, Malaysia. - [``](headers/beniaich2023.md) — Beniaich et al. (2023), soil-water PTFs for four Moroccan regions. - [``](headers/chakraborty2011.md) — Chakraborty et al. (2011), point water-retention PTFs for Indian soils. +- [``](headers/clapp1978.md) — Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. - [``](headers/cosby1984.md) — Cosby et al. (1984), United States. - [``](headers/dharumarajan2019.md) — Dharumarajan et al. (2019) hydraulic PTFs for the Karnataka Plateau. - [``](headers/ferrerjulia2004.md) — Ferrer Julià et al. (2004), saturated-conductivity PTFs for Spanish soils. diff --git a/docs/src/reference/cpp/functions.md b/docs/src/reference/cpp/functions.md index 8e07987..371647f 100644 --- a/docs/src/reference/cpp/functions.md +++ b/docs/src/reference/cpp/functions.md @@ -27,6 +27,7 @@ title: C++ function index | [`ptfkit::chakraborty2011::calc_ptf_chakraborty2011_eq4`](modules/chakraborty2011.md#function-calc_ptf_chakraborty2011_eq4) | Estimate four gravimetric water contents from clay, silt, and sand. | [`ptfkit.chakraborty2011`](modules/chakraborty2011.md) | | [`ptfkit::chakraborty2011::calc_ptf_chakraborty2011_eq5`](modules/chakraborty2011.md#function-calc_ptf_chakraborty2011_eq5) | Estimate four gravimetric water contents from clay, silt, sand, and bulk density. | [`ptfkit.chakraborty2011`](modules/chakraborty2011.md) | | [`ptfkit::chakraborty2011::calc_ptf_chakraborty2011_eq6`](modules/chakraborty2011.md#function-calc_ptf_chakraborty2011_eq6) | Estimate four gravimetric water contents from texture, organic carbon, and bulk density. | [`ptfkit.chakraborty2011`](modules/chakraborty2011.md) | +| [`ptfkit::clapp1978::calc_ptf_clapp1978`](modules/clapp1978.md#function-calc_ptf_clapp1978) | Return representative soil hydraulic parameters for a USDA texture class. | [`ptfkit.clapp1978`](modules/clapp1978.md) | | [`ptfkit::cosby1984::calc_ptf_cosby1984_univariate`](modules/cosby1984.md#function-calc_ptf_cosby1984_univariate) | Estimate Cosby et al. (1984) univariate hydraulic parameter statistics from soil texture. | [`ptfkit.cosby1984`](modules/cosby1984.md) | | [`ptfkit::dharumarajan2019::calc_ptf_dharumarajan2019_infiltration`](modules/dharumarajan2019.md#function-calc_ptf_dharumarajan2019_infiltration) | Estimate infiltration rate for Karnataka soils from texture fractions. | [`ptfkit.dharumarajan2019`](modules/dharumarajan2019.md) | | [`ptfkit::dharumarajan2019::calc_ptf_dharumarajan2019_nkp`](modules/dharumarajan2019.md#function-calc_ptf_dharumarajan2019_nkp) | Estimate field capacity and wilting point for Northern Karnataka soils. | [`ptfkit.dharumarajan2019`](modules/dharumarajan2019.md) | diff --git a/docs/src/reference/cpp/index.md b/docs/src/reference/cpp/index.md index f32a2e3..13a70c7 100644 --- a/docs/src/reference/cpp/index.md +++ b/docs/src/reference/cpp/index.md @@ -15,6 +15,7 @@ ptfkit's C++ API is organized around C++20 modules. - [`ptfkit.aimrun2009`](modules/aimrun2009.md) — Aimrun & Amin (2009), Tanjung Karang Rice Irrigation Project, Malaysia. - [`ptfkit.beniaich2023`](modules/beniaich2023.md) — Beniaich et al. (2023), soil-water PTFs for four Moroccan regions. - [`ptfkit.chakraborty2011`](modules/chakraborty2011.md) — Chakraborty et al. (2011), point water-retention PTFs for Indian soils. +- [`ptfkit.clapp1978`](modules/clapp1978.md) — Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. - [`ptfkit.cosby1984`](modules/cosby1984.md) — Cosby et al. (1984), United States. - [`ptfkit.dharumarajan2019`](modules/dharumarajan2019.md) — Dharumarajan et al. (2019) hydraulic PTFs for the Karnataka Plateau. - [`ptfkit.ferrerjulia2004`](modules/ferrerjulia2004.md) — Ferrer Julià et al. (2004), saturated-conductivity PTFs for Spanish soils. diff --git a/docs/src/reference/cpp/modules/clapp1978.md b/docs/src/reference/cpp/modules/clapp1978.md new file mode 100644 index 0000000..a42c17e --- /dev/null +++ b/docs/src/reference/cpp/modules/clapp1978.md @@ -0,0 +1,119 @@ +--- +# @generated by ptfkit-codegen; DO NOT EDIT. + +title: C++ module ptfkit.clapp1978 +nav-title: ptfkit.clapp1978 +--- + +# `ptfkit.clapp1978` + +```cpp +import ptfkit.clapp1978; +``` + +**Exported namespace:** `ptfkit::clapp1978` + +Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. + +## Source + +Clapp, R. B., & Hornberger, G. M. (1978). Empirical equations for some soil hydraulic properties. Water Resources Research. + +## Scope + +**Territory:** United States + +**Dataset:** Desorption data reported by Holtan et al. (1968) for soils collected at 34 United States localities; 1,446 soils remained from an initial set of more than 1,800 after the exclusions described by the paper. + +[PTF catalog page](../../../ptf-catalog/sources/clapp1978.md) + +## `UsdaTextureClass` + +```cpp +enum class UsdaTextureClass { + Sand, + LoamySand, + SandyLoam, + SiltLoam, + Loam, + SandyClayLoam, + SiltyClayLoam, + ClayLoam, + SandyClay, + SiltyClay, + Clay, +}; +``` + +| Member | Canonical value | Description | +| --- | --- | --- | +| `Sand` | `sand` | USDA sand soil textural class. | +| `LoamySand` | `loamy sand` | USDA loamy sand soil textural class. | +| `SandyLoam` | `sandy loam` | USDA sandy loam soil textural class. | +| `SiltLoam` | `silt loam` | USDA silt loam soil textural class. | +| `Loam` | `loam` | USDA loam soil textural class. | +| `SandyClayLoam` | `sandy clay loam` | USDA sandy clay loam soil textural class. | +| `SiltyClayLoam` | `silty clay loam` | USDA silty clay loam soil textural class. | +| `ClayLoam` | `clay loam` | USDA clay loam soil textural class. | +| `SandyClay` | `sandy clay` | USDA sandy clay soil textural class. | +| `SiltyClay` | `silty clay` | USDA silty clay soil textural class. | +| `Clay` | `clay` | USDA clay soil textural class. | + +## `Clapp1978Parameters` + +```cpp +struct Clapp1978Parameters { + double b; + double saturation_suction; + double wetting_front_suction; + double saturated_water_content; + double saturated_hydraulic_conductivity; + double sorptivity; +}; +``` + +| Field | Description | +| --- | --- | +| `b` | Mean exponent of the moisture-characteristic power curve. (1) | +| `saturation_suction` | Representative saturation suction, calculated as the antilog of the mean logarithm of fitted saturation-suction values. (cm) | +| `wetting_front_suction` | Representative Green-Ampt wetting-front suction. (cm) | +| `saturated_water_content` | Mean saturated volumetric water content, taken as total porosity. (1) | +| `saturated_hydraulic_conductivity` | Mean saturated hydraulic conductivity reported by Li et al. (1976). (cm/min) | +| `sorptivity` | Representative sorptivity for the paper's single initial moisture deficit corresponding to 500-cm initial suction. (cm/min^(1/2)) | + +## Functions + +### `calc_ptf_clapp1978` {#function-calc_ptf_clapp1978} + +Return representative soil hydraulic parameters for a USDA texture class. + +```cpp +[[nodiscard]] +inline Clapp1978Parameters calc_ptf_clapp1978(UsdaTextureClass soil_texture) +``` + +#### Parameters + +| Name | Description | +| --- | --- | +| `soil_texture` | USDA soil textural class used to select a row of Table 2. | + +#### Returns + +A `Clapp1978Parameters` value. + +!!! note + + The representative wetting-front suctions use an inflection wetness of 0.92 to model gradual air entry near saturation. + +!!! note + + The sorptivity values use the average saturated hydraulic conductivities reported by Li et al. (1976) and one initial moisture deficit at 500-cm suction. + +!!! warning + + The paper states that these unverified average values should not be used blindly because saturation suction varies substantially within texture classes. + +!!! warning + + Sorptivity values must be treated cautiously because the conductivity averages may not represent the same average soils as the other parameters. diff --git a/docs/src/reference/cpp/modules/ptfkit.md b/docs/src/reference/cpp/modules/ptfkit.md index 2025080..19f9d28 100644 --- a/docs/src/reference/cpp/modules/ptfkit.md +++ b/docs/src/reference/cpp/modules/ptfkit.md @@ -19,6 +19,7 @@ This umbrella module re-exports every public ptfkit source module. Import an ind - [`ptfkit.aimrun2009`](aimrun2009.md) — Aimrun & Amin (2009), Tanjung Karang Rice Irrigation Project, Malaysia. - [`ptfkit.beniaich2023`](beniaich2023.md) — Beniaich et al. (2023), soil-water PTFs for four Moroccan regions. - [`ptfkit.chakraborty2011`](chakraborty2011.md) — Chakraborty et al. (2011), point water-retention PTFs for Indian soils. +- [`ptfkit.clapp1978`](clapp1978.md) — Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. - [`ptfkit.cosby1984`](cosby1984.md) — Cosby et al. (1984), United States. - [`ptfkit.dharumarajan2019`](dharumarajan2019.md) — Dharumarajan et al. (2019) hydraulic PTFs for the Karnataka Plateau. - [`ptfkit.ferrerjulia2004`](ferrerjulia2004.md) — Ferrer Julià et al. (2004), saturated-conductivity PTFs for Spanish soils. diff --git a/docs/src/reference/python/clapp1978.md b/docs/src/reference/python/clapp1978.md new file mode 100644 index 0000000..2b81575 --- /dev/null +++ b/docs/src/reference/python/clapp1978.md @@ -0,0 +1,8 @@ +--- +# @generated by ptfkit-codegen; DO NOT EDIT. + +title: Python module ptfkit.clapp1978 +nav-title: ptfkit.clapp1978 +--- + +::: ptfkit.clapp1978 diff --git a/docs/src/reference/python/index.md b/docs/src/reference/python/index.md index d5f4cb6..a9ec82f 100644 --- a/docs/src/reference/python/index.md +++ b/docs/src/reference/python/index.md @@ -14,6 +14,7 @@ ptfkit's Python API is organized around public source modules. - [`ptfkit.aimrun2009`](aimrun2009.md) — Aimrun & Amin (2009), Tanjung Karang Rice Irrigation Project, Malaysia. - [`ptfkit.beniaich2023`](beniaich2023.md) — Beniaich et al. (2023), soil-water PTFs for four Moroccan regions. - [`ptfkit.chakraborty2011`](chakraborty2011.md) — Chakraborty et al. (2011), point water-retention PTFs for Indian soils. +- [`ptfkit.clapp1978`](clapp1978.md) — Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. - [`ptfkit.cosby1984`](cosby1984.md) — Cosby et al. (1984), United States. - [`ptfkit.dharumarajan2019`](dharumarajan2019.md) — Dharumarajan et al. (2019) hydraulic PTFs for the Karnataka Plateau. - [`ptfkit.ferrerjulia2004`](ferrerjulia2004.md) — Ferrer Julià et al. (2004), saturated-conductivity PTFs for Spanish soils. diff --git a/specs/functions/clapp1978.yaml b/specs/functions/clapp1978.yaml new file mode 100644 index 0000000..19d4745 --- /dev/null +++ b/specs/functions/clapp1978.yaml @@ -0,0 +1,268 @@ +source: + summary: Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. + citation_apa: >- + Clapp, R. B., & Hornberger, G. M. (1978). Empirical equations for some + soil hydraulic properties. Water Resources Research. + doi: null +scope: + territory: United States + dataset: >- + Desorption data reported by Holtan et al. (1968) for soils collected at 34 + United States localities; 1,446 soils remained from an initial set of more + than 1,800 after the exclusions described by the paper. +$defs: + UsdaTextureClass: + type: enum + description: USDA soil textural class used to select a row of Table 2. + values: + - name: sand + value: "sand" + description: USDA sand soil textural class. + - name: loamy_sand + value: "loamy sand" + description: USDA loamy sand soil textural class. + - name: sandy_loam + value: "sandy loam" + description: USDA sandy loam soil textural class. + - name: silt_loam + value: "silt loam" + description: USDA silt loam soil textural class. + - name: loam + value: "loam" + description: USDA loam soil textural class. + - name: sandy_clay_loam + value: "sandy clay loam" + description: USDA sandy clay loam soil textural class. + - name: silty_clay_loam + value: "silty clay loam" + description: USDA silty clay loam soil textural class. + - name: clay_loam + value: "clay loam" + description: USDA clay loam soil textural class. + - name: sandy_clay + value: "sandy clay" + description: USDA sandy clay soil textural class. + - name: silty_clay + value: "silty clay" + description: USDA silty clay soil textural class. + - name: clay + value: "clay" + description: USDA clay soil textural class. + Clapp1978Parameters: + type: record + name: Clapp1978Parameters + fields: + - name: b + symbol: b + unit: "1" + domain: value > 0 + description: Mean exponent of the moisture-characteristic power curve. + - name: saturation_suction + symbol: psi_s(log) + unit: cm + domain: value > 0 + description: >- + Representative saturation suction, calculated as the antilog of the + mean logarithm of fitted saturation-suction values. + - name: wetting_front_suction + symbol: psi_f + unit: cm + domain: value > 0 + description: Representative Green-Ampt wetting-front suction. + - name: saturated_water_content + symbol: theta_s + unit: "1" + domain: 0 < value < 1 + description: Mean saturated volumetric water content, taken as total porosity. + - name: saturated_hydraulic_conductivity + symbol: K_s + unit: cm/min + domain: value > 0 + description: Mean saturated hydraulic conductivity reported by Li et al. (1976). + - name: sorptivity + symbol: S + unit: cm/min^(1/2) + domain: value > 0 + description: >- + Representative sorptivity for the paper's single initial moisture + deficit corresponding to 500-cm initial suction. + Clapp1978ParametersByTexture: + type: lookup + input: + $ref: "#/$defs/UsdaTextureClass" + output: + $ref: "#/$defs/Clapp1978Parameters" + values: + - key: sand + value: {b: 4.05, saturation_suction: 3.50, wetting_front_suction: 4.66, saturated_water_content: 0.395, saturated_hydraulic_conductivity: 1.056, sorptivity: 1.52} + - key: loamy_sand + value: {b: 4.38, saturation_suction: 1.78, wetting_front_suction: 2.38, saturated_water_content: 0.410, saturated_hydraulic_conductivity: 0.938, sorptivity: 1.04} + - key: sandy_loam + value: {b: 4.90, saturation_suction: 7.18, wetting_front_suction: 9.52, saturated_water_content: 0.435, saturated_hydraulic_conductivity: 0.208, sorptivity: 1.03} + - key: silt_loam + value: {b: 5.30, saturation_suction: 56.6, wetting_front_suction: 75.3, saturated_water_content: 0.485, saturated_hydraulic_conductivity: 0.0432, sorptivity: 1.26} + - key: loam + value: {b: 5.39, saturation_suction: 14.6, wetting_front_suction: 20.0, saturated_water_content: 0.451, saturated_hydraulic_conductivity: 0.0417, sorptivity: 0.693} + - key: sandy_clay_loam + value: {b: 7.12, saturation_suction: 8.63, wetting_front_suction: 11.7, saturated_water_content: 0.420, saturated_hydraulic_conductivity: 0.0378, sorptivity: 0.488} + - key: silty_clay_loam + value: {b: 7.75, saturation_suction: 14.6, wetting_front_suction: 19.7, saturated_water_content: 0.477, saturated_hydraulic_conductivity: 0.0102, sorptivity: 0.310} + - key: clay_loam + value: {b: 8.52, saturation_suction: 36.1, wetting_front_suction: 48.1, saturated_water_content: 0.476, saturated_hydraulic_conductivity: 0.0147, sorptivity: 0.537} + - key: sandy_clay + value: {b: 10.4, saturation_suction: 6.16, wetting_front_suction: 8.18, saturated_water_content: 0.426, saturated_hydraulic_conductivity: 0.0130, sorptivity: 0.223} + - key: silty_clay + value: {b: 10.4, saturation_suction: 17.4, wetting_front_suction: 23.0, saturated_water_content: 0.492, saturated_hydraulic_conductivity: 0.0062, sorptivity: 0.242} + - key: clay + value: {b: 11.4, saturation_suction: 18.6, wetting_front_suction: 24.3, saturated_water_content: 0.482, saturated_hydraulic_conductivity: 0.0077, sorptivity: 0.268} +functions: + - name: calc_ptf_clapp1978 + status: implemented + public_api: + name: calc_ptf_clapp1978 + result_class: Clapp1978Parameters + summary: Return representative soil hydraulic parameters for a USDA texture class. + scope: + prediction_target: >- + Representative moisture-characteristic exponent, saturation suction, + wetting-front suction, saturated water content, saturated hydraulic + conductivity, and sorptivity for a soil textural class. + models: + h_theta: >- + Power-curve moisture characteristic with a parabolic gradual-air-entry + section near saturation + k_h: Campbell relative-conductivity power relation + inputs: + - $ref: "#/$defs/UsdaTextureClass" + name: soil_texture + description: USDA soil textural class used to select a row of Table 2. + outputs: + $ref: "#/$defs/Clapp1978Parameters" + golden_tests: + - id: table_2_sand + inputs: {soil_texture: sand} + expected: {b: 4.05, saturation_suction: 3.50, wetting_front_suction: 4.66, saturated_water_content: 0.395, saturated_hydraulic_conductivity: 1.056, sorptivity: 1.52} + rtol: 0.0 + atol: 0.0 + notes: Direct representative values for Sand from Table 2. + - id: table_2_loamy_sand + inputs: {soil_texture: loamy_sand} + expected: {b: 4.38, saturation_suction: 1.78, wetting_front_suction: 2.38, saturated_water_content: 0.410, saturated_hydraulic_conductivity: 0.938, sorptivity: 1.04} + rtol: 0.0 + atol: 0.0 + notes: Direct representative values for Loamy sand from Table 2. + - id: table_2_sandy_loam + inputs: {soil_texture: sandy_loam} + expected: {b: 4.90, saturation_suction: 7.18, wetting_front_suction: 9.52, saturated_water_content: 0.435, saturated_hydraulic_conductivity: 0.208, sorptivity: 1.03} + rtol: 0.0 + atol: 0.0 + notes: Direct representative values for Sandy loam from Table 2. + - id: table_2_silt_loam + inputs: {soil_texture: silt_loam} + expected: {b: 5.30, saturation_suction: 56.6, wetting_front_suction: 75.3, saturated_water_content: 0.485, saturated_hydraulic_conductivity: 0.0432, sorptivity: 1.26} + rtol: 0.0 + atol: 0.0 + notes: Direct representative values for Silt loam from Table 2. + - id: table_2_loam + inputs: {soil_texture: loam} + expected: {b: 5.39, saturation_suction: 14.6, wetting_front_suction: 20.0, saturated_water_content: 0.451, saturated_hydraulic_conductivity: 0.0417, sorptivity: 0.693} + rtol: 0.0 + atol: 0.0 + notes: Direct representative values for Loam from Table 2. + - id: table_2_sandy_clay_loam + inputs: {soil_texture: sandy_clay_loam} + expected: {b: 7.12, saturation_suction: 8.63, wetting_front_suction: 11.7, saturated_water_content: 0.420, saturated_hydraulic_conductivity: 0.0378, sorptivity: 0.488} + rtol: 0.0 + atol: 0.0 + notes: Direct representative values for Sandy clay loam from Table 2. + - id: table_2_silty_clay_loam + inputs: {soil_texture: silty_clay_loam} + expected: {b: 7.75, saturation_suction: 14.6, wetting_front_suction: 19.7, saturated_water_content: 0.477, saturated_hydraulic_conductivity: 0.0102, sorptivity: 0.310} + rtol: 0.0 + atol: 0.0 + notes: Direct representative values for Silty clay loam from Table 2. + - id: table_2_clay_loam + inputs: {soil_texture: clay_loam} + expected: {b: 8.52, saturation_suction: 36.1, wetting_front_suction: 48.1, saturated_water_content: 0.476, saturated_hydraulic_conductivity: 0.0147, sorptivity: 0.537} + rtol: 0.0 + atol: 0.0 + notes: Direct representative values for Clay loam from Table 2. + - id: table_2_sandy_clay + inputs: {soil_texture: sandy_clay} + expected: {b: 10.4, saturation_suction: 6.16, wetting_front_suction: 8.18, saturated_water_content: 0.426, saturated_hydraulic_conductivity: 0.0130, sorptivity: 0.223} + rtol: 0.0 + atol: 0.0 + notes: Direct representative values for Sandy clay from Table 2. + - id: table_2_silty_clay + inputs: {soil_texture: silty_clay} + expected: {b: 10.4, saturation_suction: 17.4, wetting_front_suction: 23.0, saturated_water_content: 0.492, saturated_hydraulic_conductivity: 0.0062, sorptivity: 0.242} + rtol: 0.0 + atol: 0.0 + notes: Direct representative values for Silty clay from Table 2. + - id: table_2_clay + inputs: {soil_texture: clay} + expected: {b: 11.4, saturation_suction: 18.6, wetting_front_suction: 24.3, saturated_water_content: 0.482, saturated_hydraulic_conductivity: 0.0077, sorptivity: 0.268} + rtol: 0.0 + atol: 0.0 + notes: Direct representative values for Clay from Table 2. + edge_cases: [] + documentation: + notes: + - >- + The representative wetting-front suctions use an inflection wetness of + 0.92 to model gradual air entry near saturation. + - >- + The sorptivity values use the average saturated hydraulic conductivities + reported by Li et al. (1976) and one initial moisture deficit at 500-cm + suction. + warnings: + - >- + The paper states that these unverified average values should not be used + blindly because saturation suction varies substantially within texture classes. + - >- + Sorptivity values must be treated cautiously because the conductivity + averages may not represent the same average soils as the other parameters. + implementation: + variables: + - name: parameters + lookup: + table: + $ref: "#/$defs/Clapp1978ParametersByTexture" + key: soil_texture + +scientific_notes: | + # Clapp and Hornberger (1978) + + ## Supported models + + - Table 2 gives representative hydraulic parameters for 11 USDA soil textural + classes. The prepared source handoff identifies the class lookup and its six + outputs as the PTF represented by the paper. + - The moisture characteristic is `psi = psi_s W^(-b)`, where + `W = theta / theta_s`, away from the gradual-air-entry region. Near saturation, + the paper joins this power curve to a parabola through an inflection point. + - The prepared correction distinguishes Campbell's theoretical relation + `k = W^(2b+2)` from the Clapp-Hornberger empirical working relation + `k = W^(2b+3)`. The latter governs the representative calculations. + + ## Review decisions + + - Preserve the Table 2 `psi_s(log)` column as `saturation_suction`. It is the + antilog of the mean log of fitted saturation-suction values, not a logarithmic + output, and is reported in centimeters. + - Preserve saturated water content as a dimensionless volumetric fraction. The + prose defines it as volumetric water content at saturation or total porosity; + the supplied table's unit glyph is damaged. + - `UsdaTextureClass` preserves a stable schema identifier and the canonical lowercase + USDA textual value for each class. Lookup rows reference member identifiers; + numeric target ordinals are not part of the source specification. + + ## Documented limitations + + - The fitted data are desorption measurements, although wetting-front suction + describes imbibition. The paper applies no hysteresis correction because the + applicability of proposed corrections across the investigated soils is unclear. + - The paper deleted rocky soils, fits with `b > 25`, and soils whose calculated + wetness exceeded one at 0.1-bar tension before computing the class statistics. + - Texture strongly indicates `b`, but the paper states that texture is not a good + indicator of saturation suction because within-class variability is large. diff --git a/targets/ptfkit-native/cpp/clapp1978.cppm b/targets/ptfkit-native/cpp/clapp1978.cppm new file mode 100644 index 0000000..d06f995 --- /dev/null +++ b/targets/ptfkit-native/cpp/clapp1978.cppm @@ -0,0 +1,168 @@ +/* @generated by ptfkit-codegen; DO NOT EDIT. */ + +module; +#include +#include + +export module ptfkit.clapp1978; + +/** + * @brief Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. + * + * @details Source publication: + * Clapp, R. B., & Hornberger, G. M. (1978). Empirical equations for some soil hydraulic + * properties. Water Resources Research. + * + * @remark Geographic scope: + * United States + * + * @remark Calibration dataset: + * Desorption data reported by Holtan et al. (1968) for soils collected at 34 United States + * localities; 1,446 soils remained from an initial set of more than 1,800 after the exclusions + * described by the paper. + */ + +export namespace ptfkit::clapp1978 { + +enum class UsdaTextureClass { + /** + * @brief USDA sand soil textural class. + */ + Sand, + /** + * @brief USDA loamy sand soil textural class. + */ + LoamySand, + /** + * @brief USDA sandy loam soil textural class. + */ + SandyLoam, + /** + * @brief USDA silt loam soil textural class. + */ + SiltLoam, + /** + * @brief USDA loam soil textural class. + */ + Loam, + /** + * @brief USDA sandy clay loam soil textural class. + */ + SandyClayLoam, + /** + * @brief USDA silty clay loam soil textural class. + */ + SiltyClayLoam, + /** + * @brief USDA clay loam soil textural class. + */ + ClayLoam, + /** + * @brief USDA sandy clay soil textural class. + */ + SandyClay, + /** + * @brief USDA silty clay soil textural class. + */ + SiltyClay, + /** + * @brief USDA clay soil textural class. + */ + Clay, +}; + +struct Clapp1978Parameters { + /** + * @brief Mean exponent of the moisture-characteristic power curve. (1) + */ + double b; + /** + * @brief Representative saturation suction, calculated as the antilog of the mean logarithm of + * fitted saturation-suction values. (cm) + */ + double saturation_suction; + /** + * @brief Representative Green-Ampt wetting-front suction. (cm) + */ + double wetting_front_suction; + /** + * @brief Mean saturated volumetric water content, taken as total porosity. (1) + */ + double saturated_water_content; + /** + * @brief Mean saturated hydraulic conductivity reported by Li et al. (1976). (cm/min) + */ + double saturated_hydraulic_conductivity; + /** + * @brief Representative sorptivity for the paper's single initial moisture deficit + * corresponding to 500-cm initial suction. (cm/min^(1/2)) + */ + double sorptivity; +}; + +[[nodiscard]] inline Clapp1978Parameters +clapp1978_parameters_from_usda_texture_class(UsdaTextureClass value) { + switch (value) { + case UsdaTextureClass::Sand: + return Clapp1978Parameters{4.05, 3.5, 4.66, 0.395, 1.056, 1.52}; + case UsdaTextureClass::LoamySand: + return Clapp1978Parameters{4.38, 1.78, 2.38, 0.41, 0.938, 1.04}; + case UsdaTextureClass::SandyLoam: + return Clapp1978Parameters{4.9, 7.18, 9.52, 0.435, 0.208, 1.03}; + case UsdaTextureClass::SiltLoam: + return Clapp1978Parameters{5.3, 56.6, 75.3, 0.485, 0.0432, 1.26}; + case UsdaTextureClass::Loam: + return Clapp1978Parameters{5.39, 14.6, 20.0, 0.451, 0.0417, 0.693}; + case UsdaTextureClass::SandyClayLoam: + return Clapp1978Parameters{7.12, 8.63, 11.7, 0.42, 0.0378, 0.488}; + case UsdaTextureClass::SiltyClayLoam: + return Clapp1978Parameters{7.75, 14.6, 19.7, 0.477, 0.0102, 0.31}; + case UsdaTextureClass::ClayLoam: + return Clapp1978Parameters{8.52, 36.1, 48.1, 0.476, 0.0147, 0.537}; + case UsdaTextureClass::SandyClay: + return Clapp1978Parameters{10.4, 6.16, 8.18, 0.426, 0.013, 0.223}; + case UsdaTextureClass::SiltyClay: + return Clapp1978Parameters{10.4, 17.4, 23.0, 0.492, 0.0062, 0.242}; + case UsdaTextureClass::Clay: + return Clapp1978Parameters{11.4, 18.6, 24.3, 0.482, 0.0077, 0.268}; + default: + std::unreachable(); + } +} + +/** + * @brief Return representative soil hydraulic parameters for a USDA texture class. + * @param soil_texture USDA soil textural class used to select a row of Table 2. + * @return A result with the following fields: + * - `b` — Mean exponent of the moisture-characteristic power curve. (1) + * - `saturation_suction` — Representative saturation suction, calculated as the antilog of + * the mean logarithm of fitted saturation-suction values. (cm) + * - `wetting_front_suction` — Representative Green-Ampt wetting-front suction. (cm) + * - `saturated_water_content` — Mean saturated volumetric water content, taken as total + * porosity. (1) + * - `saturated_hydraulic_conductivity` — Mean saturated hydraulic conductivity reported by + * Li et al. (1976). (cm/min) + * - `sorptivity` — Representative sorptivity for the paper's single initial moisture deficit + * corresponding to 500-cm initial suction. (cm/min^(1/2)) + * + * @details Prediction target: + * Representative moisture-characteristic exponent, saturation suction, wetting-front suction, + * saturated water content, saturated hydraulic conductivity, and sorptivity for a soil + * textural class. + * @note The representative wetting-front suctions use an inflection wetness of 0.92 to model + * gradual air entry near saturation. + * @note The sorptivity values use the average saturated hydraulic conductivities reported by + * Li et al. (1976) and one initial moisture deficit at 500-cm suction. + * @warning The paper states that these unverified average values should not be used blindly + * because saturation suction varies substantially within texture classes. + * @warning Sorptivity values must be treated cautiously because the conductivity averages may + * not represent the same average soils as the other parameters. + */ +[[nodiscard]] +inline Clapp1978Parameters calc_ptf_clapp1978(UsdaTextureClass soil_texture) { + const Clapp1978Parameters parameters = + clapp1978_parameters_from_usda_texture_class(soil_texture); + return parameters; +} + +} // namespace ptfkit::clapp1978 diff --git a/targets/ptfkit-native/cpp/ptfkit.cppm b/targets/ptfkit-native/cpp/ptfkit.cppm index 02fff9a..b2f524c 100644 --- a/targets/ptfkit-native/cpp/ptfkit.cppm +++ b/targets/ptfkit-native/cpp/ptfkit.cppm @@ -6,6 +6,7 @@ export import ptfkit.ahuja1984; export import ptfkit.aimrun2009; export import ptfkit.beniaich2023; export import ptfkit.chakraborty2011; +export import ptfkit.clapp1978; export import ptfkit.cosby1984; export import ptfkit.dharumarajan2019; export import ptfkit.ferrerjulia2004; diff --git a/targets/ptfkit-native/include/ptfkit/clapp1978.h b/targets/ptfkit-native/include/ptfkit/clapp1978.h new file mode 100644 index 0000000..0d5f8a6 --- /dev/null +++ b/targets/ptfkit-native/include/ptfkit/clapp1978.h @@ -0,0 +1,165 @@ +/* @generated by ptfkit-codegen; DO NOT EDIT. */ + +#ifndef PTFKIT_CLAPP1978_H +#define PTFKIT_CLAPP1978_H + +#include +#include + +/** + * @brief Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. + * + * @details Source publication: + * Clapp, R. B., & Hornberger, G. M. (1978). Empirical equations for some soil hydraulic + * properties. Water Resources Research. + * + * @remark Geographic scope: + * United States + * + * @remark Calibration dataset: + * Desorption data reported by Holtan et al. (1968) for soils collected at 34 United States + * localities; 1,446 soils remained from an initial set of more than 1,800 after the exclusions + * described by the paper. + */ + +typedef enum { + /** + * @brief USDA sand soil textural class. + */ + clapp1978_usda_texture_class_sand, + /** + * @brief USDA loamy sand soil textural class. + */ + clapp1978_usda_texture_class_loamy_sand, + /** + * @brief USDA sandy loam soil textural class. + */ + clapp1978_usda_texture_class_sandy_loam, + /** + * @brief USDA silt loam soil textural class. + */ + clapp1978_usda_texture_class_silt_loam, + /** + * @brief USDA loam soil textural class. + */ + clapp1978_usda_texture_class_loam, + /** + * @brief USDA sandy clay loam soil textural class. + */ + clapp1978_usda_texture_class_sandy_clay_loam, + /** + * @brief USDA silty clay loam soil textural class. + */ + clapp1978_usda_texture_class_silty_clay_loam, + /** + * @brief USDA clay loam soil textural class. + */ + clapp1978_usda_texture_class_clay_loam, + /** + * @brief USDA sandy clay soil textural class. + */ + clapp1978_usda_texture_class_sandy_clay, + /** + * @brief USDA silty clay soil textural class. + */ + clapp1978_usda_texture_class_silty_clay, + /** + * @brief USDA clay soil textural class. + */ + clapp1978_usda_texture_class_clay, +} clapp1978_usda_texture_class; + +typedef struct { + /** + * @brief Mean exponent of the moisture-characteristic power curve. (1) + */ + double b; + /** + * @brief Representative saturation suction, calculated as the antilog of the mean logarithm of + * fitted saturation-suction values. (cm) + */ + double saturation_suction; + /** + * @brief Representative Green-Ampt wetting-front suction. (cm) + */ + double wetting_front_suction; + /** + * @brief Mean saturated volumetric water content, taken as total porosity. (1) + */ + double saturated_water_content; + /** + * @brief Mean saturated hydraulic conductivity reported by Li et al. (1976). (cm/min) + */ + double saturated_hydraulic_conductivity; + /** + * @brief Representative sorptivity for the paper's single initial moisture deficit + * corresponding to 500-cm initial suction. (cm/min^(1/2)) + */ + double sorptivity; +} clapp1978_parameters; + +static inline clapp1978_parameters +clapp1978_parameters_from_usda_texture_class(clapp1978_usda_texture_class value) { + switch (value) { + case clapp1978_usda_texture_class_sand: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, 4.05, 3.5, 4.66, 0.395, 1.056, 1.52); + case clapp1978_usda_texture_class_loamy_sand: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, 4.38, 1.78, 2.38, 0.41, 0.938, 1.04); + case clapp1978_usda_texture_class_sandy_loam: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, 4.9, 7.18, 9.52, 0.435, 0.208, 1.03); + case clapp1978_usda_texture_class_silt_loam: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, 5.3, 56.6, 75.3, 0.485, 0.0432, 1.26); + case clapp1978_usda_texture_class_loam: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, 5.39, 14.6, 20.0, 0.451, 0.0417, 0.693); + case clapp1978_usda_texture_class_sandy_clay_loam: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, 7.12, 8.63, 11.7, 0.42, 0.0378, 0.488); + case clapp1978_usda_texture_class_silty_clay_loam: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, 7.75, 14.6, 19.7, 0.477, 0.0102, 0.31); + case clapp1978_usda_texture_class_clay_loam: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, 8.52, 36.1, 48.1, 0.476, 0.0147, 0.537); + case clapp1978_usda_texture_class_sandy_clay: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, 10.4, 6.16, 8.18, 0.426, 0.013, 0.223); + case clapp1978_usda_texture_class_silty_clay: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, 10.4, 17.4, 23.0, 0.492, 0.0062, 0.242); + case clapp1978_usda_texture_class_clay: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, 11.4, 18.6, 24.3, 0.482, 0.0077, 0.268); + default: + return PTFKIT_RECORD_LITERAL(clapp1978_parameters, NAN, NAN, NAN, NAN, NAN, NAN); + } +} + +/** + * @brief Return representative soil hydraulic parameters for a USDA texture class. + * @param soil_texture USDA soil textural class used to select a row of Table 2. + * @return A result with the following fields: + * - `b` — Mean exponent of the moisture-characteristic power curve. (1) + * - `saturation_suction` — Representative saturation suction, calculated as the antilog of + * the mean logarithm of fitted saturation-suction values. (cm) + * - `wetting_front_suction` — Representative Green-Ampt wetting-front suction. (cm) + * - `saturated_water_content` — Mean saturated volumetric water content, taken as total + * porosity. (1) + * - `saturated_hydraulic_conductivity` — Mean saturated hydraulic conductivity reported by + * Li et al. (1976). (cm/min) + * - `sorptivity` — Representative sorptivity for the paper's single initial moisture deficit + * corresponding to 500-cm initial suction. (cm/min^(1/2)) + * + * @details Prediction target: + * Representative moisture-characteristic exponent, saturation suction, wetting-front suction, + * saturated water content, saturated hydraulic conductivity, and sorptivity for a soil + * textural class. + * @note The representative wetting-front suctions use an inflection wetness of 0.92 to model + * gradual air entry near saturation. + * @note The sorptivity values use the average saturated hydraulic conductivities reported by + * Li et al. (1976) and one initial moisture deficit at 500-cm suction. + * @warning The paper states that these unverified average values should not be used blindly + * because saturation suction varies substantially within texture classes. + * @warning Sorptivity values must be treated cautiously because the conductivity averages may + * not represent the same average soils as the other parameters. + */ +static inline clapp1978_parameters calc_ptf_clapp1978(clapp1978_usda_texture_class soil_texture) { + const clapp1978_parameters parameters = + clapp1978_parameters_from_usda_texture_class(soil_texture); + return parameters; +} + +#endif diff --git a/targets/ptfkit-native/include/ptfkit/ptfkit.h b/targets/ptfkit-native/include/ptfkit/ptfkit.h index cd74044..2ed27a4 100644 --- a/targets/ptfkit-native/include/ptfkit/ptfkit.h +++ b/targets/ptfkit-native/include/ptfkit/ptfkit.h @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include diff --git a/targets/ptfkit-native/tests/c/clapp1978.c b/targets/ptfkit-native/tests/c/clapp1978.c new file mode 100644 index 0000000..3b0291f --- /dev/null +++ b/targets/ptfkit-native/tests/c/clapp1978.c @@ -0,0 +1,115 @@ +/* @generated by ptfkit-codegen; DO NOT EDIT. */ + +#include +#include "close_enough.h" + +int main() { + { + const clapp1978_parameters result = calc_ptf_clapp1978(clapp1978_usda_texture_class_sand); + assert_close_enough(result.b, 4.05, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 3.5, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 4.66, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.395, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 1.056, 0.0, 0.0); + assert_close_enough(result.sorptivity, 1.52, 0.0, 0.0); + } + { + const clapp1978_parameters result = + calc_ptf_clapp1978(clapp1978_usda_texture_class_loamy_sand); + assert_close_enough(result.b, 4.38, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 1.78, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 2.38, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.41, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.938, 0.0, 0.0); + assert_close_enough(result.sorptivity, 1.04, 0.0, 0.0); + } + { + const clapp1978_parameters result = + calc_ptf_clapp1978(clapp1978_usda_texture_class_sandy_loam); + assert_close_enough(result.b, 4.9, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 7.18, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 9.52, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.435, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.208, 0.0, 0.0); + assert_close_enough(result.sorptivity, 1.03, 0.0, 0.0); + } + { + const clapp1978_parameters result = + calc_ptf_clapp1978(clapp1978_usda_texture_class_silt_loam); + assert_close_enough(result.b, 5.3, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 56.6, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 75.3, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.485, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0432, 0.0, 0.0); + assert_close_enough(result.sorptivity, 1.26, 0.0, 0.0); + } + { + const clapp1978_parameters result = calc_ptf_clapp1978(clapp1978_usda_texture_class_loam); + assert_close_enough(result.b, 5.39, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 14.6, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 20.0, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.451, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0417, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.693, 0.0, 0.0); + } + { + const clapp1978_parameters result = + calc_ptf_clapp1978(clapp1978_usda_texture_class_sandy_clay_loam); + assert_close_enough(result.b, 7.12, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 8.63, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 11.7, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.42, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0378, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.488, 0.0, 0.0); + } + { + const clapp1978_parameters result = + calc_ptf_clapp1978(clapp1978_usda_texture_class_silty_clay_loam); + assert_close_enough(result.b, 7.75, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 14.6, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 19.7, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.477, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0102, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.31, 0.0, 0.0); + } + { + const clapp1978_parameters result = + calc_ptf_clapp1978(clapp1978_usda_texture_class_clay_loam); + assert_close_enough(result.b, 8.52, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 36.1, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 48.1, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.476, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0147, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.537, 0.0, 0.0); + } + { + const clapp1978_parameters result = + calc_ptf_clapp1978(clapp1978_usda_texture_class_sandy_clay); + assert_close_enough(result.b, 10.4, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 6.16, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 8.18, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.426, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.013, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.223, 0.0, 0.0); + } + { + const clapp1978_parameters result = + calc_ptf_clapp1978(clapp1978_usda_texture_class_silty_clay); + assert_close_enough(result.b, 10.4, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 17.4, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 23.0, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.492, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0062, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.242, 0.0, 0.0); + } + { + const clapp1978_parameters result = calc_ptf_clapp1978(clapp1978_usda_texture_class_clay); + assert_close_enough(result.b, 11.4, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 18.6, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 24.3, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.482, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0077, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.268, 0.0, 0.0); + } + return 0; +} diff --git a/targets/ptfkit-native/tests/cpp/clapp1978.cpp b/targets/ptfkit-native/tests/cpp/clapp1978.cpp new file mode 100644 index 0000000..be33e06 --- /dev/null +++ b/targets/ptfkit-native/tests/cpp/clapp1978.cpp @@ -0,0 +1,146 @@ +/* @generated by ptfkit-codegen; DO NOT EDIT. */ + +#ifdef IMPORT_UMBRELLA +import ptfkit; +#else +import ptfkit.clapp1978; +#endif + +#include "close_enough.h" +#include + +int main() { + { + const auto result = + ptfkit::clapp1978::calc_ptf_clapp1978(ptfkit::clapp1978::UsdaTextureClass::Sand); + static_assert(std::is_same_v, + ptfkit::clapp1978::Clapp1978Parameters>); + assert_close_enough(result.b, 4.05, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 3.5, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 4.66, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.395, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 1.056, 0.0, 0.0); + assert_close_enough(result.sorptivity, 1.52, 0.0, 0.0); + } + { + const auto result = + ptfkit::clapp1978::calc_ptf_clapp1978(ptfkit::clapp1978::UsdaTextureClass::LoamySand); + static_assert(std::is_same_v, + ptfkit::clapp1978::Clapp1978Parameters>); + assert_close_enough(result.b, 4.38, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 1.78, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 2.38, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.41, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.938, 0.0, 0.0); + assert_close_enough(result.sorptivity, 1.04, 0.0, 0.0); + } + { + const auto result = + ptfkit::clapp1978::calc_ptf_clapp1978(ptfkit::clapp1978::UsdaTextureClass::SandyLoam); + static_assert(std::is_same_v, + ptfkit::clapp1978::Clapp1978Parameters>); + assert_close_enough(result.b, 4.9, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 7.18, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 9.52, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.435, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.208, 0.0, 0.0); + assert_close_enough(result.sorptivity, 1.03, 0.0, 0.0); + } + { + const auto result = + ptfkit::clapp1978::calc_ptf_clapp1978(ptfkit::clapp1978::UsdaTextureClass::SiltLoam); + static_assert(std::is_same_v, + ptfkit::clapp1978::Clapp1978Parameters>); + assert_close_enough(result.b, 5.3, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 56.6, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 75.3, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.485, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0432, 0.0, 0.0); + assert_close_enough(result.sorptivity, 1.26, 0.0, 0.0); + } + { + const auto result = + ptfkit::clapp1978::calc_ptf_clapp1978(ptfkit::clapp1978::UsdaTextureClass::Loam); + static_assert(std::is_same_v, + ptfkit::clapp1978::Clapp1978Parameters>); + assert_close_enough(result.b, 5.39, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 14.6, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 20.0, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.451, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0417, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.693, 0.0, 0.0); + } + { + const auto result = ptfkit::clapp1978::calc_ptf_clapp1978( + ptfkit::clapp1978::UsdaTextureClass::SandyClayLoam); + static_assert(std::is_same_v, + ptfkit::clapp1978::Clapp1978Parameters>); + assert_close_enough(result.b, 7.12, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 8.63, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 11.7, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.42, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0378, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.488, 0.0, 0.0); + } + { + const auto result = ptfkit::clapp1978::calc_ptf_clapp1978( + ptfkit::clapp1978::UsdaTextureClass::SiltyClayLoam); + static_assert(std::is_same_v, + ptfkit::clapp1978::Clapp1978Parameters>); + assert_close_enough(result.b, 7.75, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 14.6, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 19.7, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.477, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0102, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.31, 0.0, 0.0); + } + { + const auto result = + ptfkit::clapp1978::calc_ptf_clapp1978(ptfkit::clapp1978::UsdaTextureClass::ClayLoam); + static_assert(std::is_same_v, + ptfkit::clapp1978::Clapp1978Parameters>); + assert_close_enough(result.b, 8.52, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 36.1, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 48.1, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.476, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0147, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.537, 0.0, 0.0); + } + { + const auto result = + ptfkit::clapp1978::calc_ptf_clapp1978(ptfkit::clapp1978::UsdaTextureClass::SandyClay); + static_assert(std::is_same_v, + ptfkit::clapp1978::Clapp1978Parameters>); + assert_close_enough(result.b, 10.4, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 6.16, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 8.18, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.426, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.013, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.223, 0.0, 0.0); + } + { + const auto result = + ptfkit::clapp1978::calc_ptf_clapp1978(ptfkit::clapp1978::UsdaTextureClass::SiltyClay); + static_assert(std::is_same_v, + ptfkit::clapp1978::Clapp1978Parameters>); + assert_close_enough(result.b, 10.4, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 17.4, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 23.0, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.492, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0062, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.242, 0.0, 0.0); + } + { + const auto result = + ptfkit::clapp1978::calc_ptf_clapp1978(ptfkit::clapp1978::UsdaTextureClass::Clay); + static_assert(std::is_same_v, + ptfkit::clapp1978::Clapp1978Parameters>); + assert_close_enough(result.b, 11.4, 0.0, 0.0); + assert_close_enough(result.saturation_suction, 18.6, 0.0, 0.0); + assert_close_enough(result.wetting_front_suction, 24.3, 0.0, 0.0); + assert_close_enough(result.saturated_water_content, 0.482, 0.0, 0.0); + assert_close_enough(result.saturated_hydraulic_conductivity, 0.0077, 0.0, 0.0); + assert_close_enough(result.sorptivity, 0.268, 0.0, 0.0); + } + return 0; +} diff --git a/targets/ptfkit-py/src/ptfkit/_ptfkit.pyi b/targets/ptfkit-py/src/ptfkit/_ptfkit.pyi index d73443e..589523c 100644 --- a/targets/ptfkit-py/src/ptfkit/_ptfkit.pyi +++ b/targets/ptfkit-py/src/ptfkit/_ptfkit.pyi @@ -21,6 +21,7 @@ calc_ptf_chakraborty2011_eq3: ufunc calc_ptf_chakraborty2011_eq4: ufunc calc_ptf_chakraborty2011_eq5: ufunc calc_ptf_chakraborty2011_eq6: ufunc +calc_ptf_clapp1978: ufunc calc_ptf_cosby1984_univariate: ufunc calc_ptf_dharumarajan2019_infiltration: ufunc calc_ptf_dharumarajan2019_nkp: ufunc diff --git a/targets/ptfkit-py/src/ptfkit/clapp1978.c b/targets/ptfkit-py/src/ptfkit/clapp1978.c new file mode 100644 index 0000000..6d8dde6 --- /dev/null +++ b/targets/ptfkit-py/src/ptfkit/clapp1978.c @@ -0,0 +1,69 @@ +/* @generated by ptfkit-codegen; DO NOT EDIT. */ +#include +#include "ufunc.h" + +static const int calc_ptf_clapp1978_types[] = {NPY_UINT32, NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE, + NPY_DOUBLE, NPY_DOUBLE, NPY_DOUBLE}; +static int calc_ptf_clapp1978_contiguous_loop(PyArrayMethod_Context *context, char *const *data, + const npy_intp *dimensions, const npy_intp *strides, + NpyAuxData *transferdata) { + (void)context; + (void)strides; + (void)transferdata; + const npy_uint32 *in_soil_texture = (const npy_uint32 *)data[0]; + double *out_b = (double *)data[1]; + double *out_saturation_suction = (double *)data[2]; + double *out_wetting_front_suction = (double *)data[3]; + double *out_saturated_water_content = (double *)data[4]; + double *out_saturated_hydraulic_conductivity = (double *)data[5]; + double *out_sorptivity = (double *)data[6]; + for (npy_intp index = 0; index < dimensions[0]; index++) { + const npy_uint32 soil_texture = in_soil_texture[index]; + const clapp1978_parameters ptfkit_result = calc_ptf_clapp1978(soil_texture); + out_b[index] = ptfkit_result.b; + out_saturation_suction[index] = ptfkit_result.saturation_suction; + out_wetting_front_suction[index] = ptfkit_result.wetting_front_suction; + out_saturated_water_content[index] = ptfkit_result.saturated_water_content; + out_saturated_hydraulic_conductivity[index] = + ptfkit_result.saturated_hydraulic_conductivity; + out_sorptivity[index] = ptfkit_result.sorptivity; + } + return 0; +} + +static int calc_ptf_clapp1978_strided_loop(PyArrayMethod_Context *context, char *const *data, + const npy_intp *dimensions, const npy_intp *strides, + NpyAuxData *transferdata) { + (void)context; + (void)transferdata; + for (npy_intp index = 0; index < dimensions[0]; index++) { + const npy_uint32 soil_texture = *(const npy_uint32 *)(data[0] + index * strides[0]); + const clapp1978_parameters ptfkit_result = calc_ptf_clapp1978(soil_texture); + *(double *)(data[1] + index * strides[1]) = ptfkit_result.b; + *(double *)(data[2] + index * strides[2]) = ptfkit_result.saturation_suction; + *(double *)(data[3] + index * strides[3]) = ptfkit_result.wetting_front_suction; + *(double *)(data[4] + index * strides[4]) = ptfkit_result.saturated_water_content; + *(double *)(data[5] + index * strides[5]) = ptfkit_result.saturated_hydraulic_conductivity; + *(double *)(data[6] + index * strides[6]) = ptfkit_result.sorptivity; + } + return 0; +} +static PyType_Slot calc_ptf_clapp1978_slots[] = { + {NPY_METH_strided_loop, calc_ptf_clapp1978_strided_loop}, + {NPY_METH_contiguous_loop, calc_ptf_clapp1978_contiguous_loop}, + {0, NULL}, +}; +static PyArrayMethod_Spec calc_ptf_clapp1978_spec = { + .name = "calc_ptf_clapp1978", + .nin = 1, + .nout = 6, + .casting = NPY_SAME_KIND_CASTING, + .slots = calc_ptf_clapp1978_slots, +}; + +int ptfkit_register_clapp1978(PyObject *module) { + if (ptfkit_add_ufunc(module, "calc_ptf_clapp1978", calc_ptf_clapp1978_types, 1, 6, + &calc_ptf_clapp1978_spec) < 0) + return -1; + return 0; +} diff --git a/targets/ptfkit-py/src/ptfkit/clapp1978.py b/targets/ptfkit-py/src/ptfkit/clapp1978.py new file mode 100644 index 0000000..a71ed35 --- /dev/null +++ b/targets/ptfkit-py/src/ptfkit/clapp1978.py @@ -0,0 +1,171 @@ +# @generated by ptfkit-codegen; DO NOT EDIT. + +r"""Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. + +Reference: + Clapp, R. B., & Hornberger, G. M. (1978). Empirical equations for some soil hydraulic + properties. Water Resources Research. + +Territory + +: United States + +Dataset + +: Desorption data reported by Holtan et al. (1968) for soils collected at 34 United States + localities; 1,446 soils remained from an initial set of more than 1,800 after the exclusions + described by the paper. + +""" + +from __future__ import annotations + +from enum import Enum +from typing import TYPE_CHECKING, Generic, NamedTuple, TypeVar, overload + +from ptfkit._dispatch import call as _call +from ptfkit._ptfkit import ( + calc_ptf_clapp1978 as _calc_ptf_clapp1978, +) +from ptfkit.enums import EnumArray + + +if TYPE_CHECKING: + from collections.abc import Iterable + + from numpy import floating, uint32 + from numpy.typing import NDArray + + +class UsdaTextureClass(Enum): + """USDA soil textural class used to select a row of Table 2. + + Attributes: + SAND: USDA sand soil textural class. + LOAMY_SAND: USDA loamy sand soil textural class. + SANDY_LOAM: USDA sandy loam soil textural class. + SILT_LOAM: USDA silt loam soil textural class. + LOAM: USDA loam soil textural class. + SANDY_CLAY_LOAM: USDA sandy clay loam soil textural class. + SILTY_CLAY_LOAM: USDA silty clay loam soil textural class. + CLAY_LOAM: USDA clay loam soil textural class. + SANDY_CLAY: USDA sandy clay soil textural class. + SILTY_CLAY: USDA silty clay soil textural class. + CLAY: USDA clay soil textural class. + + """ + + SAND = 'sand' + LOAMY_SAND = 'loamy sand' + SANDY_LOAM = 'sandy loam' + SILT_LOAM = 'silt loam' + LOAM = 'loam' + SANDY_CLAY_LOAM = 'sandy clay loam' + SILTY_CLAY_LOAM = 'silty clay loam' + CLAY_LOAM = 'clay loam' + SANDY_CLAY = 'sandy clay' + SILTY_CLAY = 'silty clay' + CLAY = 'clay' + + @classmethod + def array(cls, values: Iterable[UsdaTextureClass]) -> EnumArray[UsdaTextureClass]: + """Encode members once as a reusable typed enum array.""" + return EnumArray._from_members(cls, values) # noqa: SLF001 + + +def _encode_calc_ptf_clapp1978_soil_texture( + value: UsdaTextureClass | EnumArray[UsdaTextureClass], +) -> uint32 | NDArray[uint32]: + if isinstance(value, UsdaTextureClass): + return EnumArray._encode_member(UsdaTextureClass, value) # noqa: SLF001 + if isinstance(value, EnumArray): + return value._codes_for(UsdaTextureClass) # noqa: SLF001 + message = 'expected UsdaTextureClass or EnumArray[UsdaTextureClass]' + raise TypeError(message) + + +T = TypeVar('T') + + +class Clapp1978Parameters(NamedTuple, Generic[T]): + """Results returned by the matching PTF. + + Attributes: + b: Mean exponent of the moisture-characteristic power curve. (1) + saturation_suction: Representative saturation suction, calculated as the antilog of the mean + logarithm of fitted saturation-suction values. (cm) + wetting_front_suction: Representative Green-Ampt wetting-front suction. (cm) + saturated_water_content: Mean saturated volumetric water content, taken as total porosity. + (1) + saturated_hydraulic_conductivity: Mean saturated hydraulic conductivity reported by Li et + al. (1976). (cm/min) + sorptivity: Representative sorptivity for the paper's single initial moisture deficit + corresponding to 500-cm initial suction. (cm/min^(1/2)) + + """ + + b: T + saturation_suction: T + wetting_front_suction: T + saturated_water_content: T + saturated_hydraulic_conductivity: T + sorptivity: T + + +__all__ = ['Clapp1978Parameters', 'UsdaTextureClass', 'calc_ptf_clapp1978'] + + +@overload +def calc_ptf_clapp1978(*, soil_texture: UsdaTextureClass) -> Clapp1978Parameters[floating]: ... + + +@overload +def calc_ptf_clapp1978( + *, + soil_texture: EnumArray[UsdaTextureClass], + out: Clapp1978Parameters[NDArray[floating]] | None = None, +) -> Clapp1978Parameters[NDArray[floating]]: ... + + +def calc_ptf_clapp1978( + *, + soil_texture: UsdaTextureClass | EnumArray[UsdaTextureClass], + out: Clapp1978Parameters[NDArray[floating]] | None = None, +) -> Clapp1978Parameters[floating] | Clapp1978Parameters[NDArray[floating]]: + r"""Return representative soil hydraulic parameters for a USDA texture class. + + Arguments: + soil_texture: USDA soil textural class used to select a row of Table 2. + out: Optional output arrays for in-place calculation. + + Returns: + Clapp1978Parameters: Results grouped by result attributes. + + Models: + $h(\theta)$: Power-curve moisture characteristic with a parabolic gradual-air-entry section + near saturation + $k(h)$: Campbell relative-conductivity power relation + + Notes: + Prediction target: Representative moisture-characteristic exponent, saturation suction, + wetting-front suction, saturated water content, saturated hydraulic conductivity, and + sorptivity for a soil textural class. + The representative wetting-front suctions use an inflection wetness of 0.92 to model gradual + air entry near saturation. + The sorptivity values use the average saturated hydraulic conductivities reported by Li et + al. (1976) and one initial moisture deficit at 500-cm suction. + + Warning: + The paper states that these unverified average values should not be used blindly because + saturation suction varies substantially within texture classes. + Sorptivity values must be treated cautiously because the conductivity averages may not + represent the same average soils as the other parameters. + + """ + values = _call( + _calc_ptf_clapp1978, + _encode_calc_ptf_clapp1978_soil_texture(soil_texture), + out=out, + ) + + return Clapp1978Parameters(*values) diff --git a/targets/ptfkit-py/src/ptfkit/ptfkit.c b/targets/ptfkit-py/src/ptfkit/ptfkit.c index e687583..5ba70bb 100644 --- a/targets/ptfkit-py/src/ptfkit/ptfkit.c +++ b/targets/ptfkit-py/src/ptfkit/ptfkit.c @@ -10,6 +10,7 @@ #include "aimrun2009.c" #include "beniaich2023.c" #include "chakraborty2011.c" +#include "clapp1978.c" #include "cosby1984.c" #include "dharumarajan2019.c" #include "ferrerjulia2004.c" @@ -52,6 +53,10 @@ PyMODINIT_FUNC PyInit__ptfkit(void) { Py_DECREF(module); return NULL; } + if (ptfkit_register_clapp1978(module) < 0) { + Py_DECREF(module); + return NULL; + } if (ptfkit_register_cosby1984(module) < 0) { Py_DECREF(module); return NULL; diff --git a/targets/ptfkit-py/tests/test_clapp1978.py b/targets/ptfkit-py/tests/test_clapp1978.py new file mode 100644 index 0000000..5b3975a --- /dev/null +++ b/targets/ptfkit-py/tests/test_clapp1978.py @@ -0,0 +1,220 @@ +# @generated by ptfkit-codegen; DO NOT EDIT. +from __future__ import annotations + +import pytest + +from _helpers import prepare_vector_case +from ptfkit.clapp1978 import Clapp1978Parameters, UsdaTextureClass, calc_ptf_clapp1978 + + +CASES_CALC_PTF_CLAPP1978 = [ + ( + {'soil_texture': UsdaTextureClass.SAND}, + { + 'b': 4.05, + 'saturated_hydraulic_conductivity': 1.056, + 'saturated_water_content': 0.395, + 'saturation_suction': 3.5, + 'sorptivity': 1.52, + 'wetting_front_suction': 4.66, + }, + 0.0, + 0.0, + ), + ( + {'soil_texture': UsdaTextureClass.LOAMY_SAND}, + { + 'b': 4.38, + 'saturated_hydraulic_conductivity': 0.938, + 'saturated_water_content': 0.41, + 'saturation_suction': 1.78, + 'sorptivity': 1.04, + 'wetting_front_suction': 2.38, + }, + 0.0, + 0.0, + ), + ( + {'soil_texture': UsdaTextureClass.SANDY_LOAM}, + { + 'b': 4.9, + 'saturated_hydraulic_conductivity': 0.208, + 'saturated_water_content': 0.435, + 'saturation_suction': 7.18, + 'sorptivity': 1.03, + 'wetting_front_suction': 9.52, + }, + 0.0, + 0.0, + ), + ( + {'soil_texture': UsdaTextureClass.SILT_LOAM}, + { + 'b': 5.3, + 'saturated_hydraulic_conductivity': 0.0432, + 'saturated_water_content': 0.485, + 'saturation_suction': 56.6, + 'sorptivity': 1.26, + 'wetting_front_suction': 75.3, + }, + 0.0, + 0.0, + ), + ( + {'soil_texture': UsdaTextureClass.LOAM}, + { + 'b': 5.39, + 'saturated_hydraulic_conductivity': 0.0417, + 'saturated_water_content': 0.451, + 'saturation_suction': 14.6, + 'sorptivity': 0.693, + 'wetting_front_suction': 20.0, + }, + 0.0, + 0.0, + ), + ( + {'soil_texture': UsdaTextureClass.SANDY_CLAY_LOAM}, + { + 'b': 7.12, + 'saturated_hydraulic_conductivity': 0.0378, + 'saturated_water_content': 0.42, + 'saturation_suction': 8.63, + 'sorptivity': 0.488, + 'wetting_front_suction': 11.7, + }, + 0.0, + 0.0, + ), + ( + {'soil_texture': UsdaTextureClass.SILTY_CLAY_LOAM}, + { + 'b': 7.75, + 'saturated_hydraulic_conductivity': 0.0102, + 'saturated_water_content': 0.477, + 'saturation_suction': 14.6, + 'sorptivity': 0.31, + 'wetting_front_suction': 19.7, + }, + 0.0, + 0.0, + ), + ( + {'soil_texture': UsdaTextureClass.CLAY_LOAM}, + { + 'b': 8.52, + 'saturated_hydraulic_conductivity': 0.0147, + 'saturated_water_content': 0.476, + 'saturation_suction': 36.1, + 'sorptivity': 0.537, + 'wetting_front_suction': 48.1, + }, + 0.0, + 0.0, + ), + ( + {'soil_texture': UsdaTextureClass.SANDY_CLAY}, + { + 'b': 10.4, + 'saturated_hydraulic_conductivity': 0.013, + 'saturated_water_content': 0.426, + 'saturation_suction': 6.16, + 'sorptivity': 0.223, + 'wetting_front_suction': 8.18, + }, + 0.0, + 0.0, + ), + ( + {'soil_texture': UsdaTextureClass.SILTY_CLAY}, + { + 'b': 10.4, + 'saturated_hydraulic_conductivity': 0.0062, + 'saturated_water_content': 0.492, + 'saturation_suction': 17.4, + 'sorptivity': 0.242, + 'wetting_front_suction': 23.0, + }, + 0.0, + 0.0, + ), + ( + {'soil_texture': UsdaTextureClass.CLAY}, + { + 'b': 11.4, + 'saturated_hydraulic_conductivity': 0.0077, + 'saturated_water_content': 0.482, + 'saturation_suction': 18.6, + 'sorptivity': 0.268, + 'wetting_front_suction': 24.3, + }, + 0.0, + 0.0, + ), +] + + +@pytest.mark.parametrize(('inputs', 'expected', 'rtol', 'atol'), CASES_CALC_PTF_CLAPP1978) +def test_calc_ptf_clapp1978_golden( + inputs: dict[str, object], expected: dict[str, float], rtol: float, atol: float +): + result = calc_ptf_clapp1978(**inputs) # ty: ignore[no-matching-overload] + + assert result.b == pytest.approx(expected['b'], rel=rtol, abs=atol) + assert result.saturation_suction == pytest.approx( + expected['saturation_suction'], rel=rtol, abs=atol + ) + assert result.wetting_front_suction == pytest.approx( + expected['wetting_front_suction'], rel=rtol, abs=atol + ) + assert result.saturated_water_content == pytest.approx( + expected['saturated_water_content'], rel=rtol, abs=atol + ) + assert result.saturated_hydraulic_conductivity == pytest.approx( + expected['saturated_hydraulic_conductivity'], rel=rtol, abs=atol + ) + assert result.sorptivity == pytest.approx(expected['sorptivity'], rel=rtol, abs=atol) + + +def test_calc_ptf_clapp1978_array(): + inputs, expected, rtol, atol, _out = prepare_vector_case( + CASES_CALC_PTF_CLAPP1978, Clapp1978Parameters + ) + result = calc_ptf_clapp1978(**inputs, out=None) + assert result.b[0] == pytest.approx(expected['b'], rel=rtol, abs=atol) + assert result.saturation_suction[0] == pytest.approx( + expected['saturation_suction'], rel=rtol, abs=atol + ) + assert result.wetting_front_suction[0] == pytest.approx( + expected['wetting_front_suction'], rel=rtol, abs=atol + ) + assert result.saturated_water_content[0] == pytest.approx( + expected['saturated_water_content'], rel=rtol, abs=atol + ) + assert result.saturated_hydraulic_conductivity[0] == pytest.approx( + expected['saturated_hydraulic_conductivity'], rel=rtol, abs=atol + ) + assert result.sorptivity[0] == pytest.approx(expected['sorptivity'], rel=rtol, abs=atol) + + +def test_calc_ptf_clapp1978_out(): + inputs, expected, rtol, atol, out = prepare_vector_case( + CASES_CALC_PTF_CLAPP1978, Clapp1978Parameters + ) + result = calc_ptf_clapp1978(**inputs, out=out) + for actual, expected_out in zip(result, out, strict=True): + assert actual is expected_out + assert result.b[0] == pytest.approx(expected['b'], rel=rtol, abs=atol) + assert result.saturation_suction[0] == pytest.approx( + expected['saturation_suction'], rel=rtol, abs=atol + ) + assert result.wetting_front_suction[0] == pytest.approx( + expected['wetting_front_suction'], rel=rtol, abs=atol + ) + assert result.saturated_water_content[0] == pytest.approx( + expected['saturated_water_content'], rel=rtol, abs=atol + ) + assert result.saturated_hydraulic_conductivity[0] == pytest.approx( + expected['saturated_hydraulic_conductivity'], rel=rtol, abs=atol + ) + assert result.sorptivity[0] == pytest.approx(expected['sorptivity'], rel=rtol, abs=atol) diff --git a/targets/ptfkit-py/tests/test_enum_array.py b/targets/ptfkit-py/tests/test_enum_array.py new file mode 100644 index 0000000..f617c34 --- /dev/null +++ b/targets/ptfkit-py/tests/test_enum_array.py @@ -0,0 +1,45 @@ +from __future__ import annotations + +from enum import Enum + +import numpy as np +import pytest + +from ptfkit.clapp1978 import UsdaTextureClass, calc_ptf_clapp1978 +from ptfkit.enums import EnumArray + + +class OtherEnum(Enum): + VALUE = 'value' + + +def test_enum_values_preserve_canonical_text() -> None: + assert UsdaTextureClass.SAND.value == 'sand' + assert UsdaTextureClass.LOAMY_SAND.value == 'loamy sand' + + +def test_enum_array_is_encoded_once_and_reusable() -> None: + textures = UsdaTextureClass.array( + [UsdaTextureClass.SAND, UsdaTextureClass.LOAMY_SAND, UsdaTextureClass.CLAY] + ) + + first = calc_ptf_clapp1978(soil_texture=textures) + codes = textures._codes_for(UsdaTextureClass) # noqa: SLF001 + second = calc_ptf_clapp1978(soil_texture=textures) + + assert first.b.tolist() == [4.05, 4.38, 11.4] + assert second.b.tolist() == first.b.tolist() + assert textures._codes_for(UsdaTextureClass) is codes # noqa: SLF001 + + +@pytest.mark.parametrize('value', ['sand', 0, np.array([0], dtype=np.uint32), OtherEnum.VALUE]) +def test_enum_input_rejects_untyped_values(value: object) -> None: + with pytest.raises(TypeError, match='expected UsdaTextureClass'): + calc_ptf_clapp1978(soil_texture=value) # ty: ignore[no-matching-overload] + + +def test_enum_input_rejects_an_array_of_another_enum() -> None: + values = EnumArray._from_members(OtherEnum, [OtherEnum.VALUE]) # noqa: SLF001 + + with pytest.raises(TypeError, match=r'expected EnumArray\[UsdaTextureClass\]'): + calc_ptf_clapp1978(soil_texture=values) diff --git a/targets/ptfkit-rs/src/clapp1978.rs b/targets/ptfkit-rs/src/clapp1978.rs new file mode 100644 index 0000000..9229840 --- /dev/null +++ b/targets/ptfkit-rs/src/clapp1978.rs @@ -0,0 +1,368 @@ +// @generated by ptfkit-codegen; DO NOT EDIT. + +#![doc = r"Clapp and Hornberger (1978) representative soil hydraulic parameters by texture. + +# Reference + +Clapp, R. B., & Hornberger, G. M. (1978). Empirical equations for some soil hydraulic +properties. Water Resources Research. + +# Territory + +United States + +# Dataset + +Desorption data reported by Holtan et al. (1968) for soils collected at 34 United States +localities; 1,446 soils remained from an initial set of more than 1,800 after the exclusions +described by the paper."] + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum UsdaTextureClass { + #[doc = r"USDA sand soil textural class."] + Sand, + #[doc = r"USDA loamy sand soil textural class."] + LoamySand, + #[doc = r"USDA sandy loam soil textural class."] + SandyLoam, + #[doc = r"USDA silt loam soil textural class."] + SiltLoam, + #[doc = r"USDA loam soil textural class."] + Loam, + #[doc = r"USDA sandy clay loam soil textural class."] + SandyClayLoam, + #[doc = r"USDA silty clay loam soil textural class."] + SiltyClayLoam, + #[doc = r"USDA clay loam soil textural class."] + ClayLoam, + #[doc = r"USDA sandy clay soil textural class."] + SandyClay, + #[doc = r"USDA silty clay soil textural class."] + SiltyClay, + #[doc = r"USDA clay soil textural class."] + Clay, +} +impl From for Clapp1978Parameters { + fn from(value: UsdaTextureClass) -> Self { + match value { + UsdaTextureClass::Sand => Self { + b: 4.05f64, + saturation_suction: 3.5f64, + wetting_front_suction: 4.66f64, + saturated_water_content: 0.395f64, + saturated_hydraulic_conductivity: 1.056f64, + sorptivity: 1.52f64, + }, + UsdaTextureClass::LoamySand => Self { + b: 4.38f64, + saturation_suction: 1.78f64, + wetting_front_suction: 2.38f64, + saturated_water_content: 0.41f64, + saturated_hydraulic_conductivity: 0.938f64, + sorptivity: 1.04f64, + }, + UsdaTextureClass::SandyLoam => Self { + b: 4.9f64, + saturation_suction: 7.18f64, + wetting_front_suction: 9.52f64, + saturated_water_content: 0.435f64, + saturated_hydraulic_conductivity: 0.208f64, + sorptivity: 1.03f64, + }, + UsdaTextureClass::SiltLoam => Self { + b: 5.3f64, + saturation_suction: 56.6f64, + wetting_front_suction: 75.3f64, + saturated_water_content: 0.485f64, + saturated_hydraulic_conductivity: 0.0432f64, + sorptivity: 1.26f64, + }, + UsdaTextureClass::Loam => Self { + b: 5.39f64, + saturation_suction: 14.6f64, + wetting_front_suction: 20.0f64, + saturated_water_content: 0.451f64, + saturated_hydraulic_conductivity: 0.0417f64, + sorptivity: 0.693f64, + }, + UsdaTextureClass::SandyClayLoam => Self { + b: 7.12f64, + saturation_suction: 8.63f64, + wetting_front_suction: 11.7f64, + saturated_water_content: 0.42f64, + saturated_hydraulic_conductivity: 0.0378f64, + sorptivity: 0.488f64, + }, + UsdaTextureClass::SiltyClayLoam => Self { + b: 7.75f64, + saturation_suction: 14.6f64, + wetting_front_suction: 19.7f64, + saturated_water_content: 0.477f64, + saturated_hydraulic_conductivity: 0.0102f64, + sorptivity: 0.31f64, + }, + UsdaTextureClass::ClayLoam => Self { + b: 8.52f64, + saturation_suction: 36.1f64, + wetting_front_suction: 48.1f64, + saturated_water_content: 0.476f64, + saturated_hydraulic_conductivity: 0.0147f64, + sorptivity: 0.537f64, + }, + UsdaTextureClass::SandyClay => Self { + b: 10.4f64, + saturation_suction: 6.16f64, + wetting_front_suction: 8.18f64, + saturated_water_content: 0.426f64, + saturated_hydraulic_conductivity: 0.013f64, + sorptivity: 0.223f64, + }, + UsdaTextureClass::SiltyClay => Self { + b: 10.4f64, + saturation_suction: 17.4f64, + wetting_front_suction: 23.0f64, + saturated_water_content: 0.492f64, + saturated_hydraulic_conductivity: 0.0062f64, + sorptivity: 0.242f64, + }, + UsdaTextureClass::Clay => Self { + b: 11.4f64, + saturation_suction: 18.6f64, + wetting_front_suction: 24.3f64, + saturated_water_content: 0.482f64, + saturated_hydraulic_conductivity: 0.0077f64, + sorptivity: 0.268f64, + }, + } + } +} +#[doc = r"Results returned by `calc_ptf_clapp1978`."] +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Clapp1978Parameters { + #[doc = r"Mean exponent of the moisture-characteristic power curve. (1)"] + pub b: f64, + #[doc = r"Representative saturation suction, calculated as the antilog of the mean logarithm of fitted +saturation-suction values. (cm)"] + pub saturation_suction: f64, + #[doc = r"Representative Green-Ampt wetting-front suction. (cm)"] + pub wetting_front_suction: f64, + #[doc = r"Mean saturated volumetric water content, taken as total porosity. (1)"] + pub saturated_water_content: f64, + #[doc = r"Mean saturated hydraulic conductivity reported by Li et al. (1976). (cm/min)"] + pub saturated_hydraulic_conductivity: f64, + #[doc = r"Representative sorptivity for the paper's single initial moisture deficit corresponding to +500-cm initial suction. (cm/min^(1/2))"] + pub sorptivity: f64, +} +#[doc = r"Return representative soil hydraulic parameters for a USDA texture class. + +# Arguments + + * soil_texture: USDA soil textural class used to select a row of Table 2. + +# Returns + +A [`Clapp1978Parameters`]. + +# Models + + * h(theta): Power-curve moisture characteristic with a parabolic gradual-air-entry section + near saturation + * k(h): Campbell relative-conductivity power relation + +# Notes + +Prediction target: Representative moisture-characteristic exponent, saturation suction, +wetting-front suction, saturated water content, saturated hydraulic conductivity, and sorptivity +for a soil textural class. +The representative wetting-front suctions use an inflection wetness of 0.92 to model gradual air +entry near saturation. +The sorptivity values use the average saturated hydraulic conductivities reported by Li et al. +(1976) and one initial moisture deficit at 500-cm suction. + +# Warnings + +The paper states that these unverified average values should not be used blindly because +saturation suction varies substantially within texture classes. +Sorptivity values must be treated cautiously because the conductivity averages may not represent +the same average soils as the other parameters."] +#[cfg_attr(feature = "inline", inline)] +#[must_use] +pub fn calc_ptf_clapp1978(soil_texture: UsdaTextureClass) -> Clapp1978Parameters { + soil_texture.into() +} +#[cfg(test)] +mod tests { + use super::*; + fn assert_close(actual: f64, expected: f64, atol: f64, rtol: f64) { + assert!( + (actual - expected).abs() <= atol + rtol * expected.abs(), + "actual {actual} != expected {expected}" + ); + } + #[test] + fn table_2_sand() { + let result = calc_ptf_clapp1978(UsdaTextureClass::Sand); + assert_close(result.b, 4.05f64, 0f64, 0f64); + assert_close(result.saturation_suction, 3.5f64, 0f64, 0f64); + assert_close(result.wetting_front_suction, 4.66f64, 0f64, 0f64); + assert_close(result.saturated_water_content, 0.395f64, 0f64, 0f64); + assert_close( + result.saturated_hydraulic_conductivity, + 1.056f64, + 0f64, + 0f64, + ); + assert_close(result.sorptivity, 1.52f64, 0f64, 0f64); + } + #[test] + fn table_2_loamy_sand() { + let result = calc_ptf_clapp1978(UsdaTextureClass::LoamySand); + assert_close(result.b, 4.38f64, 0f64, 0f64); + assert_close(result.saturation_suction, 1.78f64, 0f64, 0f64); + assert_close(result.wetting_front_suction, 2.38f64, 0f64, 0f64); + assert_close(result.saturated_water_content, 0.41f64, 0f64, 0f64); + assert_close( + result.saturated_hydraulic_conductivity, + 0.938f64, + 0f64, + 0f64, + ); + assert_close(result.sorptivity, 1.04f64, 0f64, 0f64); + } + #[test] + fn table_2_sandy_loam() { + let result = calc_ptf_clapp1978(UsdaTextureClass::SandyLoam); + assert_close(result.b, 4.9f64, 0f64, 0f64); + assert_close(result.saturation_suction, 7.18f64, 0f64, 0f64); + assert_close(result.wetting_front_suction, 9.52f64, 0f64, 0f64); + assert_close(result.saturated_water_content, 0.435f64, 0f64, 0f64); + assert_close( + result.saturated_hydraulic_conductivity, + 0.208f64, + 0f64, + 0f64, + ); + assert_close(result.sorptivity, 1.03f64, 0f64, 0f64); + } + #[test] + fn table_2_silt_loam() { + let result = calc_ptf_clapp1978(UsdaTextureClass::SiltLoam); + assert_close(result.b, 5.3f64, 0f64, 0f64); + assert_close(result.saturation_suction, 56.6f64, 0f64, 0f64); + assert_close(result.wetting_front_suction, 75.3f64, 0f64, 0f64); + assert_close(result.saturated_water_content, 0.485f64, 0f64, 0f64); + assert_close( + result.saturated_hydraulic_conductivity, + 0.0432f64, + 0f64, + 0f64, + ); + assert_close(result.sorptivity, 1.26f64, 0f64, 0f64); + } + #[test] + fn table_2_loam() { + let result = calc_ptf_clapp1978(UsdaTextureClass::Loam); + assert_close(result.b, 5.39f64, 0f64, 0f64); + assert_close(result.saturation_suction, 14.6f64, 0f64, 0f64); + assert_close(result.wetting_front_suction, 20f64, 0f64, 0f64); + assert_close(result.saturated_water_content, 0.451f64, 0f64, 0f64); + assert_close( + result.saturated_hydraulic_conductivity, + 0.0417f64, + 0f64, + 0f64, + ); + assert_close(result.sorptivity, 0.693f64, 0f64, 0f64); + } + #[test] + fn table_2_sandy_clay_loam() { + let result = calc_ptf_clapp1978(UsdaTextureClass::SandyClayLoam); + assert_close(result.b, 7.12f64, 0f64, 0f64); + assert_close(result.saturation_suction, 8.63f64, 0f64, 0f64); + assert_close(result.wetting_front_suction, 11.7f64, 0f64, 0f64); + assert_close(result.saturated_water_content, 0.42f64, 0f64, 0f64); + assert_close( + result.saturated_hydraulic_conductivity, + 0.0378f64, + 0f64, + 0f64, + ); + assert_close(result.sorptivity, 0.488f64, 0f64, 0f64); + } + #[test] + fn table_2_silty_clay_loam() { + let result = calc_ptf_clapp1978(UsdaTextureClass::SiltyClayLoam); + assert_close(result.b, 7.75f64, 0f64, 0f64); + assert_close(result.saturation_suction, 14.6f64, 0f64, 0f64); + assert_close(result.wetting_front_suction, 19.7f64, 0f64, 0f64); + assert_close(result.saturated_water_content, 0.477f64, 0f64, 0f64); + assert_close( + result.saturated_hydraulic_conductivity, + 0.0102f64, + 0f64, + 0f64, + ); + assert_close(result.sorptivity, 0.31f64, 0f64, 0f64); + } + #[test] + fn table_2_clay_loam() { + let result = calc_ptf_clapp1978(UsdaTextureClass::ClayLoam); + assert_close(result.b, 8.52f64, 0f64, 0f64); + assert_close(result.saturation_suction, 36.1f64, 0f64, 0f64); + assert_close(result.wetting_front_suction, 48.1f64, 0f64, 0f64); + assert_close(result.saturated_water_content, 0.476f64, 0f64, 0f64); + assert_close( + result.saturated_hydraulic_conductivity, + 0.0147f64, + 0f64, + 0f64, + ); + assert_close(result.sorptivity, 0.537f64, 0f64, 0f64); + } + #[test] + fn table_2_sandy_clay() { + let result = calc_ptf_clapp1978(UsdaTextureClass::SandyClay); + assert_close(result.b, 10.4f64, 0f64, 0f64); + assert_close(result.saturation_suction, 6.16f64, 0f64, 0f64); + assert_close(result.wetting_front_suction, 8.18f64, 0f64, 0f64); + assert_close(result.saturated_water_content, 0.426f64, 0f64, 0f64); + assert_close( + result.saturated_hydraulic_conductivity, + 0.013f64, + 0f64, + 0f64, + ); + assert_close(result.sorptivity, 0.223f64, 0f64, 0f64); + } + #[test] + fn table_2_silty_clay() { + let result = calc_ptf_clapp1978(UsdaTextureClass::SiltyClay); + assert_close(result.b, 10.4f64, 0f64, 0f64); + assert_close(result.saturation_suction, 17.4f64, 0f64, 0f64); + assert_close(result.wetting_front_suction, 23f64, 0f64, 0f64); + assert_close(result.saturated_water_content, 0.492f64, 0f64, 0f64); + assert_close( + result.saturated_hydraulic_conductivity, + 0.0062f64, + 0f64, + 0f64, + ); + assert_close(result.sorptivity, 0.242f64, 0f64, 0f64); + } + #[test] + fn table_2_clay() { + let result = calc_ptf_clapp1978(UsdaTextureClass::Clay); + assert_close(result.b, 11.4f64, 0f64, 0f64); + assert_close(result.saturation_suction, 18.6f64, 0f64, 0f64); + assert_close(result.wetting_front_suction, 24.3f64, 0f64, 0f64); + assert_close(result.saturated_water_content, 0.482f64, 0f64, 0f64); + assert_close( + result.saturated_hydraulic_conductivity, + 0.0077f64, + 0f64, + 0f64, + ); + assert_close(result.sorptivity, 0.268f64, 0f64, 0f64); + } +}