DEV Community

Cover image for Conditional UAV Specifications in Python: Keeping UG82 Payload and Endurance Claims Separate
UNITED UAV Official
UNITED UAV Official

Posted on

Conditional UAV Specifications in Python: Keeping UG82 Payload and Endurance Claims Separate

A technical catalog can contain individually correct numbers and still generate an incorrect sentence. The mistake often appears after ingestion: a scraper flattens a specification table, a database stores one value per attribute, and a template combines the largest values into a concise product summary. The syntax is valid. The relationship between the values has disappeared.

Consider a heavy-lift vertical takeoff and landing aircraft. A payload limit describes one constraint. An endurance statement describes a duration under a particular configuration. Putting both numbers in the same row does not establish that the aircraft achieves both simultaneously. A developer can introduce that implication without changing a single digit.

This tutorial builds a small Python boundary for that problem using the UG82 as a concrete example. It checks whether proposed structured copy preserves a reviewed listing statement, then renders that statement from controlled values. It is useful for catalog importers, quotation tools, comparison pages and systems that generate technical descriptions. The interesting engineering work is preserving meaning through transformations, rather than calculating another attractive maximum.

UG82 fixed-wing VTOL aircraft, using the unchanged product image from the UNITED UAV listing

Start with two different statements

The UG82 product listing, reviewed for this example on November 2, 2026, separately lists a maximum payload of 50 kg and endurance of up to 12 hours with an 8 kg payload. Those are vendor-declared specifications. The endurance statement includes a payload condition that must travel with the number.

A lossy representation might store max_payload_kg=50 and max_endurance_h=12, then generate “50 kg payload for 12 hours.” That sentence adds a joint operating condition the extracted evidence does not supply. A relational join on the model identifier is sufficient for assembling a product record, but it is insufficient for combining performance claims.

Represent the second statement conceptually as a tuple: model, endurance value, qualifier, payload condition and source context. The first statement belongs in a separate constraint. Their common model name allows them to appear together in a description; it does not make the maximum payload the payload used in the endurance statement.

This distinction also explains why multiplying headline endurance by headline speed is a poor catalog transformation. The result can look precise while silently joining operating points. A data pipeline should require evidence for the relationship before presenting the derived number as a product capability. The example below deliberately performs no endurance interpolation, range calculation or mission feasibility calculation.

Decide exactly what the software is allowed to conclude

The narrow question for the validator is: does this candidate reproduce the reviewed conditional listing reference? Its successful result is named listing_reference_only. That wording matters because a green result in a user interface can otherwise acquire a much broader meaning than its author intended.

A matching candidate preserves UG82, the reviewed source label, 8 kg payload, 12 hours and the up_to qualifier. A candidate combining 50 kg and 12 hours receives unsupported_joint_claim. A payload above the separately listed limit receives over_listed_payload_limit. Neither result estimates how the aircraft would behave in flight.

There is a subtle case at a lighter payload or a shorter duration. A developer might expect the checker to accept 4 kg for 12 hours, or 8 kg for 6 hours, because those values appear conservative. This implementation rejects both as transformations outside its evidence reference. It has no performance curve, fuel assumptions or rule authorizing a new published duration. That is a deliberately narrow editorial rule, not a statement that those missions are physically impossible.

The same reasoning applies to qualifiers. “Up to” is part of the claim, not decorative language that a writing template may remove. Replacing it with a statement of unconditional duration changes the meaning even if model, payload and hours remain identical. The validator checks the qualifier before allowing the renderer to produce copy.

Keep the input contract small

The candidate schema contains five fields. Every value is a string, including the quantities, and the quantity names carry their units. A caller cannot supply 8000 and rely on the receiver to guess whether it means grams or kilograms. A production importer should convert explicitly declared source units into the chosen internal units before this stage, retaining the original value and conversion record for review.

The review_label is a local editorial label. It is not a manufacturer revision number or a claim that the source page cannot change. It tells the caller which reviewed interpretation this example recognizes. If a source changes materially, the reviewer must update the constants, label and tests together. Changing only the label would create an empty appearance of provenance.

The source statement does not supply every condition needed for an operational performance assessment. Fuel load and environmental conditions remain unspecified in this extracted reference. The renderer says so. An absent condition should remain absent; silently substituting a default turns missing information into invented evidence.

Here is the complete example. Save it as ug82_guard.py and run it with Python 3.9 or later. It uses only the standard library and makes no network requests.

"""Editorial reference matching only; no flight or performance approval."""
import json
from decimal import Decimal, InvalidOperation

SOURCE_URL = "https://store.uniteduav.com/products/ug82-heavy-lift-vtol-fixed-wing-drone"
# This is our review label, not a manufacturer document revision.
REVIEW_LABEL = "listing-reviewed-2026-11-02"
MODEL = "UG82"
PAYLOAD_LIMIT_KG = Decimal("50")
REFERENCE_PAYLOAD_KG = Decimal("8")
REFERENCE_HOURS = Decimal("12")
FIELDS = {"review_label", "model", "payload_kg", "endurance_h", "qualifier"}


def unique_object(pairs):
    result = {}
    for key, value in pairs:
        if key in result:
            raise ValueError("Duplicate JSON key: " + key)
        result[key] = value
    return result


def reject_constant(value):
    raise ValueError("Nonstandard JSON numeric constant: " + value)


def load_candidate(raw):
    if not isinstance(raw, bytes) or len(raw) > 65536:
        raise ValueError("Expected at most 65536 UTF-8 bytes")
    obj = json.loads(
        raw.decode("utf-8"),
        object_pairs_hook=unique_object,
        parse_constant=reject_constant,
    )
    if not isinstance(obj, dict) or set(obj) != FIELDS:
        raise ValueError("Candidate fields do not match this schema")
    if any(type(value) is not str for value in obj.values()):
        raise ValueError("All candidate fields must be strings")
    return obj


def quantity(value, *, positive):
    if not value or len(value) > 32 or value.strip() != value:
        raise ValueError("Invalid decimal text")
    try:
        number = Decimal(value)
    except InvalidOperation as exc:
        raise ValueError("Invalid decimal text") from exc
    if not number.is_finite() or number < 0 or (positive and number == 0):
        raise ValueError("Quantity must be finite and within its sign domain")
    return number


def review_candidate(raw):
    obj = load_candidate(raw)
    payload = quantity(obj["payload_kg"], positive=False)
    hours = quantity(obj["endurance_h"], positive=True)
    if obj["model"] != MODEL:
        return "different_model"
    if obj["review_label"] != REVIEW_LABEL:
        return "reference_review_required"
    if payload > PAYLOAD_LIMIT_KG:
        return "over_listed_payload_limit"
    if obj["qualifier"] != "up_to":
        return "unsupported_qualifier"
    if payload != REFERENCE_PAYLOAD_KG or hours != REFERENCE_HOURS:
        return "unsupported_joint_claim"
    return "listing_reference_only"


def render_reference():
    # Render reviewed constants, never arbitrary request text.
    return (
        f"Vendor listing: {MODEL}, up to {REFERENCE_HOURS} h at "
        f"{REFERENCE_PAYLOAD_KG} kg payload. Separately listed maximum "
        f"payload: {PAYLOAD_LIMIT_KG} kg. Fuel load and environmental "
        "conditions are not specified in this extracted statement."
    )


if __name__ == "__main__":
    example = json.dumps({
        "review_label": REVIEW_LABEL,
        "model": MODEL,
        "payload_kg": "8",
        "endurance_h": "12",
        "qualifier": "up_to",
    }).encode("utf-8")
    status = review_candidate(example)
    print(status)
    if status == "listing_reference_only":
        print(render_reference())
        print(SOURCE_URL)
Enter fullscreen mode Exit fullscreen mode

The example prints listing_reference_only, a conditional vendor-listing sentence and its source URL. The renderer uses reviewed constants rather than inserting arbitrary candidate strings into the public description. Even after matching succeeds, user input does not become a template variable in the output sentence.

Reject malformed meaning before comparing numbers

Strict parsing has a practical role here. Two systems can interpret the same JSON object differently when a key appears twice. An input containing one payload near the beginning and another payload near the end should fail at the boundary, rather than depending on which value a downstream component retains.

The Python JSON documentation describes hooks for controlling object decoding and nonstandard numeric constants. This example uses those hooks to reject repeated keys and tokens such as NaN. It also limits the input bytes before decoding. The size bound is a useful first control; an Internet-facing service would still need its own request, execution and nesting limits.

The numeric fields then pass a separate domain check. A JSON boolean is rejected because it is not a decimal string in this schema. Text such as 8 kg is rejected because units belong in the field contract. Negative payload, zero endurance and nonfinite values fail before any comparison against the listing reference.

Using Decimal helps keep decimal input comparisons explicit. It does not make a specification accurate. The Decimal documentation also makes clear that decimal values can include special nonfinite values, which is why the code checks is_finite(). Exact representation and trustworthy evidence are different properties of a system.

Test the errors a catalog pipeline actually makes

The valuable tests are transformations that look plausible enough to escape a casual review. Combining the two maxima is one. Dropping the payload condition is another. A stale review label should also fail even when the numerical values happen to match, because the caller is referring to an interpretation that this version does not recognize.

After saving the main file, the following compact test script exercises the central behavior. The inputs are candidate publication statements, not flight observations. The 50 kg example is intentionally unsupported combined copy; it must never be displayed as a measured result.

import json
from ug82_guard import REVIEW_LABEL, review_candidate

def request(**changes):
    row = dict(review_label=REVIEW_LABEL, model="UG82",
               payload_kg="8", endurance_h="12", qualifier="up_to")
    row.update(changes)
    return json.dumps(row).encode("utf-8")

assert review_candidate(request()) == "listing_reference_only"
assert review_candidate(request(payload_kg="50")) == "unsupported_joint_claim"
assert review_candidate(request(payload_kg="4")) == "unsupported_joint_claim"
assert review_candidate(request(endurance_h="6")) == "unsupported_joint_claim"
assert review_candidate(request(payload_kg="51")) == "over_listed_payload_limit"
assert review_candidate(request(qualifier="fixed_duration")) == "unsupported_qualifier"
assert review_candidate(request(review_label="old")) == "reference_review_required"

for value in ["NaN", "Infinity", "-1", "8 kg", True]:
    try:
        review_candidate(request(payload_kg=value))
    except ValueError:
        pass
    else:
        raise AssertionError("Invalid quantity was accepted")

try:
    review_candidate(request()[:-1] + b', "payload_kg": "50"}')
except ValueError:
    pass
else:
    raise AssertionError("Duplicate JSON key was accepted")
print("Reference preservation checks passed")
Enter fullscreen mode Exit fullscreen mode

Keep rejection tests beside the renderer, not only beside the parser. A later refactor could leave validation intact while adding a template that prints a maximum payload next to a duration without its condition. A test should inspect the actual generated sentence for the payload condition, the qualifier and the explanation of unspecified conditions. Testing only that the return value is a string would miss the failure this tool exists to prevent.

Put a review boundary between extraction and generation

A useful architecture has three distinct artifacts: a captured source, a reviewed interpretation and a rendered description. The captured source establishes what was available to the reviewer. The interpretation identifies which statements and conditions were extracted. The renderer defines exactly which public language those statements support.

For a production workflow, store the source URL, retrieval time and a digest of retained source bytes alongside the reviewed interpretation. Preserve a short locator for the relevant specification paragraph or table. The digest identifies captured bytes; it does not certify a test or establish that the page is still current. Keep that distinction visible in the review interface.

Do not invalidate every description merely because an unrelated footer or tracking script changed. Detect the source change, then compare the relevant specification statement and its context. If the technical meaning changed, withdraw the old derived copy until it is reviewed. If only unrelated markup changed, retain a record of that determination. Both decisions need an accountable rule rather than a silent replacement of the stored source.

A language model can propose an extraction or a sentence, but the accepted output should still pass through the same structured checks and controlled renderer. The model should not be the only component deciding whether two independent attributes describe the same operating point. This separation also makes failures reproducible when the upstream extraction changes.

Make the result useful in a real application

In a catalog import job, schema errors should identify the affected field and preserve the rejected record for review. An unsupported joint claim should leave the last reviewed description intact or withhold the new description, according to the application's publication policy. It should not trigger an automatic fallback to the unvalidated source title, which may repeat the same ambiguity in a more compact form.

In a quotation editor, show the source statement beside the proposed sentence. A salesperson or technical reviewer needs to see why a request was rejected, not just a red icon. If someone asks for endurance at a 50 kg payload, the useful next action is to obtain configuration-specific evidence. Substituting a plausible-looking number would conceal the information gap rather than resolve it.

In continuous integration, keep a fixture for each reviewed conditional statement and a small set of adversarial transformations. Require a review when a new model identifier, qualifier, unit convention or source revision is introduced. A change to a shared template should run the fixtures for every affected product, because a generic wording improvement can remove a condition across an entire catalog.

For multi-language output, translate the condition and qualifier as part of the same unit of meaning. Do not translate the headline first and append conditions later from an optional field. The target-language renderer should require the same reviewed tuple before it produces a sentence. Numeric localization can change punctuation; it must not change the quantity, unit or relationship.

Know which evidence would justify expanding the model

The checker intentionally recognizes one listing reference. Extending it to accept a family of operating points requires more than replacing equality with a less-than comparison. The new evidence must define the relevant payload, fuel, configuration, flight profile and environmental conditions well enough to explain what the relationship means.

If a supplier provides a performance table, preserve the table's axes, notes and applicability limits. Separate tested points from analytical estimates and vendor declarations. An interpolation method would need an explicit domain, a documented assumption about the relationship and an appropriate review of uncertainty. A software library cannot manufacture those properties from two isolated maxima.

Likewise, a payload specification does not define the installed mass of every customer integration. Mounts, wiring, enclosures and power hardware can matter to a configuration assessment. This article's schema does not estimate those masses or use them to approve an aircraft. It protects the meaning of an extracted catalog statement while leaving configuration engineering to evidence that actually addresses it.

This narrowness is useful. It lets a team ship a deterministic safeguard today without pretending to have built a flight-performance model. When better evidence becomes available, the team can introduce a different reviewed rule with its own tests and output language. Existing listing references can remain distinguishable from those stronger forms of evidence.

A source link should resolve the question the code cannot

For the UG82 example, the current product page and its conditional specifications are the starting reference. A requirement involving another payload or a complete mission duration needs configuration-specific information beyond the extracted listing statement. Keeping that distinction explicit makes a technical question more precise: which configuration, which payload definition, which conditions and which evidence support the requested duration?

The same pattern applies to battery capacity and runtime, radio distance and throughput, or camera resolution and frame rate. Whenever a summary combines maxima, ask whether the evidence establishes their simultaneous applicability. Preserve the condition in structured data, keep unsupported combinations visible, and render only the statement that the reviewed source actually supports.

Disclosure: UNITED UAV sells the aircraft used in this example. The specifications discussed here are vendor-listing declarations; the code is an original illustrative editorial tool, not a report of UNITED UAV flight testing. This article was prepared with AI assistance, and the executable examples and rejection cases were run locally.

Top comments (0)