Skip to content

Add First-Class Complex Type Support #56

Description

@MuellerSeb

Add complex as a first-class scalar type in the nml-tools schema language and
support it consistently across schema validation, namelist validation,
generated Fortran, templates, documentation, and f2py/Python wrappers.

properties:
  impedance:
    type: complex
    x-fortran-kind: dp
    default: [1.0, -2.0]

This is an intentional extension of the JSON Schema type vocabulary. An
nml-tools schema remains a JSON- or YAML-serializable document and continues to
use JSON Schema concepts, but it is an nml-tools dialect rather than a strictly
conforming Draft 2020-12 schema.

The dialect and meta-schema publication work is tracked separately in #57.
Standard namelist parsing, including complex literals and complex-part designators, is described in #55.

Motivation

Fortran programmers expect complex to be an intrinsic scalar type:

complex(kind=dp) :: impedance

&solver
  impedance = (1.0, -2.0)
/

Representing this as a string, derived object, or specially marked JSON array
would preserve strict JSON Schema types at the cost of making the schema less
clear. nml-tools has evolved into a standalone Fortran schema and generation
tool with its own configuration, validation policy, operational defaults, and
x-fortran-* extensions. Its authoring model should therefore prioritize
Fortran semantics where JSON Schema has no natural equivalent.

The existing JSON Schema names should remain where they map cleanly:

  • number maps to Fortran real values;
  • integer maps to Fortran integer values;
  • boolean maps to Fortran logical values; and
  • string maps to Fortran character values.

Complex is different because JSON Schema has no scalar type that represents
it. Adding type: complex is a narrow, explicit divergence rather than a
reason to rename the existing compatible types.

Schema Syntax

Scalar Complex Values

properties:
  coefficient:
    title: Complex coefficient
    description: Coefficient in Cartesian form.
    type: complex
    x-fortran-kind: dp
    default: [1.0, 0.5]
    examples:
      - [0.0, 1.0]
      - [-2.5, 0.0]

default and each examples entry use a canonical two-element sequence:

[real-part, imaginary-part]

Both entries must be JSON numbers and must not be booleans. The sequence is a
schema-literal representation of one complex scalar; it does not make the
Fortran field an array.

The normalized Python value should be the built-in complex type. Raw parser
tokens may remain separate until schema evaluation so diagnostics, decimal
mode, and source precision are preserved.

Complex Arrays

Complex arrays use the existing single-level array schema:

properties:
  coefficients:
    type: array
    x-fortran-shape: n_coefficients
    items:
      type: complex
      x-fortran-kind: dp
    default:
      - [1.0, 0.0]
      - [0.0, 1.0]

The outer sequences represent Fortran array dimensions according to the
existing nml-tools array-default rules. Each innermost two-element sequence is
one complex scalar.

Supported Keywords

A complex scalar should support:

  • type: complex;
  • x-fortran-kind;
  • default;
  • examples;
  • title, description, and $comment; and
  • parent-object required membership under the same effective-default policy
    as other scalar types.

Reject these combinations initially:

  • x-fortran-len;
  • x-fortran-shape directly on a complex scalar;
  • properties, items, or x-fortran-type on a complex scalar;
  • minimum, maximum, exclusiveMinimum, or exclusiveMaximum, because
    complex values are unordered; and
  • enum, until exact equality, NaN, signed-zero, and generated Fortran
    comparison behavior are specified.

x-fortran-shape remains valid on an enclosing type: array whose items are
complex.

Namelist Behavior

Whole complex values use standard namelist syntax:

&solver
  coefficient = (1.0, -2.0)
  coefficients = (1.0, 0.0), (0.0, 1.0)
/

In POINT decimal mode, a comma separates the real and imaginary parts. In
COMMA decimal mode, a semicolon separates them. The complete parenthesized pair
is one effective item; a namelist null omits the complete complex value rather
than one component.

The schema-aware parser should preserve a complex literal as two source tokens
and convert it only after resolving a complex target. Complex-part designators
%re and %im belong to the parser/evaluator work and should map to the
corresponding part of the stored scalar.

The current f90nml path already returns Python built-in complex values for
whole complex literals. Complex support may therefore begin before the custom
parser migration, but complete standard designator and decimal-mode behavior
depends on that migration.

Validation And Presence

Validate a Python built-in complex as one scalar value. Schema defaults and
examples should be normalized from [real, imag] before applying generated or
runtime behavior.

Generated storage needs three distinguishable states for a complex value
without a default:

absent:   both parts contain the reserved NaN sentinel
complete: neither part contains the reserved NaN sentinel
partial:  exactly one part contains the reserved NaN sentinel
  • An absent optional value is valid and is not set.
  • An absent required value is invalid.
  • A partial value is invalid, including an optional value touched only through
    %re or %im.
  • A complete value is set and receives normal validation.

This likely requires initializing both parts with quiet NaN values and checking
the real and imaginary parts separately with ieee_is_nan. Concrete complex
values containing NaN cannot be represented independently from this sentinel
policy and should be rejected initially. Infinity policy should match the
eventual scalar real-value policy.

A configured default initializes both parts and satisfies presence according
to the general operational-default policy.

Generated Fortran

Generate scalar declarations such as:

complex(kind=dp) :: coefficient

Generate defaults using explicit component literals and kind-correct
construction, for example:

this%coefficient = cmplx(1.0_dp, -2.0_dp, kind=dp)

Required generator changes include:

  • classify complex as an intrinsic scalar category before derived-object and
    array handling;
  • include complex kinds in imports, declarations, defaults, sentinels,
    comparisons, and generated validation;
  • add complex scalar and complex-array arguments to init, set, is_set,
    and is_valid behavior;
  • add complex array allocation, assignment, reshape, runtime-dimension, and
    default handling where the corresponding intrinsic-array feature exists;
  • generate standard complex literals in namelist templates; and
  • include complex values in any generated namelist writer once that feature is
    implemented.

Flexible-tail complex arrays may remain unsupported initially if their
presence and filled-shape behavior cannot reuse the real-array implementation
without ambiguity. Any restriction must be targeted and documented.

Kind Configuration

In Fortran, the kind parameter of a complex value is the kind of its real and
imaginary components. A project may therefore use the same names for real and
complex kinds:

integer, parameter :: sp = real32
integer, parameter :: dp = real64

Other projects expose distinct aliases even when their values are equal:

integer, parameter :: sp  = real32
integer, parameter :: dp  = real64
integer, parameter :: spc = real32
integer, parameter :: dpc = real64

Extend [kinds] with an optional complex allowlist while keeping simple
configurations concise:

[kinds]
module = "mo_kind"
integer = ["i4", "i8"]
real = ["sp", "dp"]
complex = ["spc", "dpc"]

Apply this policy:

  • when kinds.complex is present, use it as the allowlist for complex schema
    properties;
  • when kinds.complex is omitted, fall back to kinds.real;
  • keep kinds.map shared across categories, because kind aliases remain
    globally resolved Fortran identifiers; and
  • require no kind configuration for an unkinded type: complex property.

The fallback permits both real(kind=dp) and complex(kind=dp) without
duplicating configuration. The explicit list permits project conventions such
as real(kind=dp) and complex(kind=dpc). The two aliases may have the same
integer value while retaining distinct names in generated source.

f2py And Python Interfaces

Python-facing APIs should use Python and NumPy complex values rather than schema
literal pairs:

cfg.set(coefficient=1.0 - 2.0j)

For arrays, accept NumPy complex arrays through the existing array-shape
normalization path. f2py supports intrinsic Fortran complex scalars and arrays;
Python scalar arguments may use built-in complex, while arrays normally use
NumPy complex64 or complex128 dtypes.

Extend the f2py configuration with a separate complex C type table:

[kinds]
module = "iso_fortran_env"
real = ["real32", "real64"]
map = { sp = "real32", dp = "real64" }

[f2py.c_types.real]
sp = "float"
dp = "double"

[f2py.c_types.complex]
sp = "complex_float"
dp = "complex_double"

The [kinds].complex fallback concerns allowed Fortran kind names only. Do not
fall back from f2py.c_types.complex to f2py.c_types.real: double and
complex_double are different f2py C representations. Require an explicit
complex mapping for every named complex kind used by generated f2py wrappers.
No mapping is needed for an unkinded complex declaration.

Extend F2pyKindUsage, F2pyCTypeMap, and .f2py_f2cmap generation with a
complex category. The generated map should contain entries such as:

dict(
    real=dict(dp="double"),
    complex=dict(dp="complex_double"),
    integer=dict(i4="int"),
)

f2py also recognizes complex_long_double, but quad-precision interoperability
is more compiler- and platform-sensitive. Treat complex_float and
complex_double as the primary mappings and only promise quad-precision
support after it passes the supported compiler matrix.

The two-element schema representation is only for JSON/YAML defaults and
examples. It should not leak into the normal Python wrapper API.

Documentation And Templates

  • Show the field type as complex, including its configured kind.
  • Render scalar defaults and examples in readable Fortran form, such as
    (1.0, -2.0), while documenting the schema pair form.
  • Render complex array examples in Fortran element order.
  • Explain that a complex value is one Fortran scalar even though its schema
    literal contains two numbers.
  • Update the README section that currently lists complex as unsupported because
    JSON lacks a native type.
  • Add the new type to the published nml-tools dialect reference and
    meta-schema.

Tests

Add focused coverage for:

  • valid and invalid scalar defaults and examples;
  • missing, extra, boolean, string, null, NaN, and infinite pair components;
  • schema rejection of bounds, length, shape, object, and array keywords on a
    complex scalar;
  • $defs and $ref reuse of complex scalar and complex-array item schemas;
  • scalar and array namelist validation from real files;
  • POINT-mode complex literals, nulls, repetition, and overlong buffers;
  • partial %re or %im assignment once the schema-aware parser supports it;
  • required, optional, defaulted, absent, complete, and partial presence states;
  • generated declarations, defaults, setters, sentinels, and validation checks;
  • fixed and runtime-sized complex arrays;
  • explicit complex kind lists and fallback to real kind lists;
  • generated templates and Markdown documentation;
  • separate f2py complex kind-map discovery and Python scalar/NumPy-array
    wrappers; and
  • generated Fortran runtime tests across the supported compiler matrix.

Run the normal project checks and update generated fixtures and examples that
gain complex fields.

Acceptance Criteria

  • type: complex is accepted as a first-class nml-tools scalar type.
  • Complex schema literals use exactly two numeric entries in Cartesian order.
  • Whole complex values validate from standard namelist syntax.
  • Internal validation and Python interfaces use built-in complex values.
  • Generated Fortran uses native complex(kind=...) storage.
  • Requiredness, defaults, optional absence, and partial component assignment
    have explicit, tested behavior.
  • Complex arrays work for the supported fixed and runtime array models.
  • [kinds].complex supports dedicated aliases and falls back to [kinds].real
    when omitted.
  • f2py wrappers expose natural Python/NumPy complex values.
  • Named complex kinds use explicit [f2py.c_types.complex] mappings.
  • The documentation identifies complex as an intentional nml-tools dialect
    extension rather than strict JSON Schema.

Out Of Scope

  • Complex ordering constraints.
  • Complex enums in the first implementation.
  • Polar schema literals.
  • Arbitrary-precision components beyond the precision already supported for
    real schema literals and Python values.
  • A namelist-to-JSON conversion API.
  • User-defined formatted I/O for application-specific complex-like types.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions