Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changeset/blue-shirts-kneel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@platforma-open/milaboratories.mixcr-shm-trees.workflow': minor
'@platforma-open/milaboratories.mixcr-shm-trees': minor
---

Replace ptransform with ptabler for all table aggregation

The by-nodes deduplication and the sequence-of-interest per-tree
aggregation now run in-process via ptabler instead of shelling out to
the ptransform binary. Integer columns are coerced back to their
declared types after the TSV read, so they are no longer float-promoted
(e.g. `192` -> `192.0`) and nulled by the Parquet importer. The
software-ptransform dependency is removed.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@
"//": {
"pnpm": {
"overrides": {
"@platforma-open/milaboratories.software-ptransform": "file:/Users/dbolotin/milab/blocks/software-ptransform/package.tgz",
"@milaboratories/graph-maker": "/Users/poslavskysv/Projects/milab/platforma/graph-maker/milaboratories-graph-maker-1.1.0.tgz",
"@platforma-sdk/model": "file:/Users/dbolotin/milab/core/platforma/sdk/model/package.tgz",
"@platforma-sdk/workflow-tengo": "file:/Users/poslavskysv/Projects/milab/platforma/platforma-sdk/sdk/workflow-tengo/package.tgz",
Expand Down
11 changes: 0 additions & 11 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ catalog:
'@platforma-open/milaboratories.software-small-binaries': ^1.15.19
'@platforma-open/milaboratories.software-mixcr': 4.7.0-303-develop
'@platforma-open/milaboratories.software-mitool': 2.3.1-24-main
'@platforma-open/milaboratories.software-ptransform': ^1.6.6

'@milaboratories/helpers': ^1.12.0

Expand Down
1 change: 0 additions & 1 deletion workflow/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
"@platforma-sdk/tengo-builder": "catalog:",
"@platforma-open/milaboratories.software-mixcr": "catalog:",
"@platforma-open/milaboratories.software-mitool": "catalog:",
"@platforma-open/milaboratories.software-ptransform": "catalog:",
"@platforma-sdk/test": "catalog:",
"vitest": "catalog:",
"typescript": "catalog:"
Expand Down
38 changes: 9 additions & 29 deletions workflow/src/soi.tpl.tengo
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ xsv := import("@platforma-sdk/workflow-tengo:pframes.xsv")
json := import("json")
pConstants := import("@platforma-sdk/workflow-tengo:pframes.constants")
soiExport := import(":soi-export")
tablesAgg := import(":tables-aggregation")

self.defineOutputs("nodesResult", "treesResult")

// import MiXCR as a software to use
mitoolSw := assets.importSoftware("@platforma-open/milaboratories.software-mitool:main")
paggregateSw := assets.importSoftware("@platforma-open/milaboratories.software-ptransform:main")

inferPartitionKeyLength := func(data) {
rType := data.info().Type.Name
Expand Down Expand Up @@ -46,7 +46,6 @@ self.body(func(inputs) {
modifiedAxesSpecs := []
importAxesSpec := []

aggregationGroupByTargets := []
aggregationImportAxesSpec := []

for idx, spec in originalAxesSpecs {
Expand All @@ -62,7 +61,6 @@ self.body(func(inputs) {

// aggregating clonal and subtree axes away
if spec.name != "pl7.app/dendrogram/subtreeId" && spec.name != "pl7.app/dendrogram/nodeId" {
aggregationGroupByTargets = append(aggregationGroupByTargets, colName)
aggregationImportAxesSpec = append(aggregationImportAxesSpec, {
column: colName,
spec: spec
Expand Down Expand Up @@ -161,32 +159,14 @@ self.body(func(inputs) {
partitionKeyLength: 0 // inferPartitionKeyLength(queryData)
}

aggregations := []
for col in resultColumns {
aggregations = append(aggregations, {
type: "first",
src: col.column,
dst: col.column
})
}

aggregationWorkflow := { steps: [ {
type: "aggregate",
groupBy: aggregationGroupByTargets,
aggregations: aggregations
} ] }

aggregateCmd := exec.builder().
printErrStreamToStdout().
software(paggregateSw).
arg("--workflow").arg("wf.json").
writeFile("wf.json", json.encode(aggregationWorkflow)).
arg("input.tsv").addFile("input.tsv", resultCsv).
arg("output.tsv").saveFile("output.tsv").
env("CIDADHOC", "1234").
run()

aggregatedCsv := aggregateCmd.getFile("output.tsv")
// Collapse subtree/node rows to one row per tree (first per group), reusing the
// ptabler dedup helper so integer columns are coerced back from the all-String
// read instead of being float-promoted by ptransform.
aggregatedCsv := tablesAgg.ensureUniqueness(
resultCsv,
tablesAgg.ensureUniquenessParamsFromPconvParams(aggregatedConvParams),
"first"
)

nodesResult := xsv.importFile(
resultCsv,
Expand Down
97 changes: 40 additions & 57 deletions workflow/src/tables-aggregation.lib.tengo
Original file line number Diff line number Diff line change
@@ -1,82 +1,65 @@
ll := import("@platforma-sdk/workflow-tengo:ll")
exec := import("@platforma-sdk/workflow-tengo:exec")
assets := import("@platforma-sdk/workflow-tengo:assets")
pt := import("@platforma-sdk/workflow-tengo:pt")
slices := import("@platforma-sdk/workflow-tengo:slices")
json := import("json")

paggregateSw := assets.importSoftware("@platforma-open/milaboratories.software-ptransform:main")

// Column name + declared type for every axis and value column, so ensureUniqueness
// knows which columns to coerce back to an integer type after reading the
// intermediate TSV as all-String.
ensureUniquenessParamsFromPconvParams := func(pfConvParams) {
return {
axes: slices.map(pfConvParams.axes, func(axis) {
return axis.column
return { column: axis.column, type: axis.spec.type }
}),
columns: slices.map(pfConvParams.columns, func(col) {
return col.column
return { column: col.column, type: col.spec.valueType }
})
}
}
Comment on lines 8 to 17

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To prevent potential runtime panics, we should defensively check if axis.spec and col.spec are defined before accessing their properties (type and valueType). In Tengo, accessing properties on an undefined value will cause a runtime panic.

ensureUniquenessParamsFromPconvParams := func(pfConvParams) {
    return {
        axes: slices.map(pfConvParams.axes, func(axis) {
            return { column: axis.column, type: is_undefined(axis.spec) ? undefined : axis.spec.type }
        }),
        columns: slices.map(pfConvParams.columns, func(col) {
            return { column: col.column, type: is_undefined(col.spec) ? undefined : col.spec.valueType }
        })
    }
}


/** Aggregating by-nodes output to make it uniquely addressable by it's native key */
/** Aggregating by-nodes output to make it uniquely addressable by its native key. */
ensureUniqueness := func(inputTsv, params, ...aggParams) {
keyColumns := params.axes
pickCols := params.columns
allCols := []
for axis in params.axes {
allCols = append(allCols, axis)
}
for col in params.columns {
allCols = append(allCols, col)
}

aggregationWorkflow := undefined
if len(aggParams) > 1 {
pickCols := []
for col in pickCols {
pickCols = append(pickCols, [
col,
col
]
)
}
keyNames := slices.map(params.axes, func(axis) { return axis.column })

rankingCol := aggParams[1]
aggregationWorkflow = {
steps: [ {
type: "aggregate",
groupBy: keyColumns,
aggregations: [ {
type: aggParams[0],
rankingCol: rankingCol,
pickCols: pickCols
} ]
} ]
}
} else {
aggregations := []
for col in pickCols {
aggregations = append(aggregations, {
type: aggParams[0],
src: col,
dst: col
})
}
wf := pt.workflow()

df := wf.frame({ file: inputTsv, xsvType: "tsv" }, { inferSchema: false })

aggregationWorkflow = { steps: [ {
type: "aggregate",
groupBy: keyColumns,
aggregations: aggregations
} ]
intCasts := []
for c in allCols {
if c.type == "Long" || c.type == "Int" {
intCasts = append(intCasts, pt.col(c.column).cast("Double").round().cast(c.type).alias(c.column))
}
}
if len(intCasts) > 0 {
df = df.withColumns(intCasts...)
}

aggregateCmd := exec.builder().
printErrStreamToStdout().
software(paggregateSw).
arg("--workflow").arg("wf.json").
writeFile("wf.json", json.encode(aggregationWorkflow)).
arg("input.tsv").addFile("input.tsv", inputTsv).
arg("output.tsv").saveFile("output.tsv").
env("CIDADHOC", "1234").
run()
result := undefined
if len(aggParams) > 1 {
// max_by: keep, per key, the row with the greatest ranking column value.
rankCol := aggParams[1]
aggs := slices.map(params.columns, func(col) {
return pt.col(col.column).maxBy(pt.col(rankCol)).alias(col.column)
})
result = df.groupBy(keyNames...).agg(aggs...)
} else {
// first: dedup rows by key, keeping the first.
result = df.unique({ subset: keyNames, keep: "first", maintainOrder: true })
}
Comment on lines +53 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The previous implementation supported arbitrary aggregation types passed via aggParams[0]. The new implementation hardcodes keep: "first" when len(aggParams) <= 1, silently ignoring any other aggregation type if specified. To prevent unexpected behavior or silent bugs, we should assert that aggParams[0] is indeed "first" when provided.

    } else {
        if len(aggParams) > 0 {
            ll.assert(aggParams[0] == "first", "Unsupported aggregation type: " + aggParams[0])
        }
        // first: dedup rows by key, keeping the first.
        result = df.unique({ subset: keyNames, keep: "first", maintainOrder: true })
    }

Comment on lines +46 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 aggParams[0] type string silently ignored

The old API used aggParams[0] as the aggregation-type discriminator ("max_by" or "first"). The new implementation discards it entirely and dispatches only on len(aggParams) > 1. A call like ensureUniqueness(tsv, params, "max_by") (type provided but ranking column omitted) now silently falls into the df.unique() branch instead of raising an error, and a hypothetical future caller passing a third aggregation type with two arguments would silently be treated as max_by. Since all current call sites happen to map 1:1 (1 arg → first, 2 args → max_by), there is no current regression, but the contract is no longer enforced.

Prompt To Fix With AI
This is a comment left during a code review.
Path: workflow/src/tables-aggregation.lib.tengo
Line: 46-56

Comment:
**`aggParams[0]` type string silently ignored**

The old API used `aggParams[0]` as the aggregation-type discriminator ("max_by" or "first"). The new implementation discards it entirely and dispatches only on `len(aggParams) > 1`. A call like `ensureUniqueness(tsv, params, "max_by")` (type provided but ranking column omitted) now silently falls into the `df.unique()` branch instead of raising an error, and a hypothetical future caller passing a third aggregation type with two arguments would silently be treated as `max_by`. Since all current call sites happen to map 1:1 (1 arg → first, 2 args → max_by), there is no current regression, but the contract is no longer enforced.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Fix in Claude Code


return aggregateCmd.getFile("output.tsv")
result.save("output.tsv")
return wf.run().getFile("output.tsv")
}

export ll.toStrict({
ensureUniqueness: ensureUniqueness,
ensureUniqueness: ensureUniqueness,
ensureUniquenessParamsFromPconvParams: ensureUniquenessParamsFromPconvParams
})
Loading