Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ import org.apache.texera.amber.operator.metadata.annotations.{

class HyperParameters[T] {

// Two rows naming one parameter emit its keyword argument twice, which the generated
// Python will not compile, so the form warns on the row rather than letting it be added.
@JsonSchemaInject(json = """{ "uniqueAmongRows": true }""")
@JsonProperty(required = true)
@JsonSchemaTitle("Parameter")
@JsonPropertyDescription("Choose the name of the parameter")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,21 @@ abstract class SklearnMLOperatorDescriptor[T <: ParamClass] extends PythonOperat
@AutofillAttributeNameList
var selectedFeatures: List[EncodableString] = _

/**
* Each row emits one keyword argument, so two rows naming one parameter emit that keyword
* twice and Python rejects the operator with a repeated-keyword SyntaxError. Refused here
* instead, where the workflow fails to compile with the parameter named.
*/
private def requireDistinctParameters(paraList: List[HyperParameters[T]]): Unit = {
val repeated = paraList.map(_.parameter.getName).groupBy(identity).collect {
case (name, rows) if rows.size > 1 => name
}
require(
repeated.isEmpty,
s"Each parameter can be set at most once. Set more than once: ${repeated.toSeq.sorted.mkString(", ")}."
)
}

private def getLoopTimes(paraList: List[HyperParameters[T]]): PythonTemplateBuilder = {
for (ele <- paraList) {
if (ele.parametersSource) {
Expand All @@ -71,6 +86,7 @@ abstract class SklearnMLOperatorDescriptor[T <: ParamClass] extends PythonOperat
}

def getParameter(paraList: List[HyperParameters[T]]): List[PythonTemplateBuilder] = {
requireDistinctParameters(paraList)
var workflowParam = s"";
var portParam = pyb"";
var paramString = pyb""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,25 @@ class SklearnAdvancedBaseDescSpec extends AnyFlatSpec with Matchers {
paramString should include("n_neighbors = int(table[")
paramString should include(".values[i]")
}

it should "refuse two rows setting one parameter, whichever source they read" in {
val d = new TestSklearnMLOp
val paraList = List(
hyperParam("n_neighbors", "int", fromWorkflow = false, value = "5"),
hyperParam("n_neighbors", "int", fromWorkflow = true, attribute = "k_col")
)
val thrown = the[IllegalArgumentException] thrownBy d.getParameter(paraList)
thrown.getMessage should include("n_neighbors")
}

it should "accept rows setting different parameters" in {
val d = new TestSklearnMLOp
val paraList = List(
hyperParam("n_neighbors", "int", fromWorkflow = false, value = "5"),
hyperParam("leaf_size", "int", fromWorkflow = false, value = "30")
)
val paramString = d.getParameter(paraList)(1).encode
paramString.filterNot(_.isWhitespace) should include("n_neighbors=int(")
paramString.filterNot(_.isWhitespace) should include("leaf_size=int(")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1794,6 +1794,36 @@ describe("OperatorPropertyEditFrameComponent", () => {
);
});

it("adds a uniqueAmongRows validator that rejects a value another row already holds", () => {
component.setFormlyFormBinding({
type: "object",
properties: {
paraList: {
type: "array",
items: {
type: "object",
properties: { parameter: { type: "string", uniqueAmongRows: true } },
},
},
},
} as CustomJSONSchema7);
// A row's own fields exist only once formly is asked to build a row.
const arrayField = getField("paraList")!;
const rowField = (arrayField.fieldArray as (root: FormlyFieldConfig) => FormlyFieldConfig)(arrayField);
const validator = rowField.fieldGroup?.find(f => f.key === "parameter")?.validators?.["uniqueAmongRows"];
expect(validator).toBeDefined();

const twoRowsSettingC = {
key: "parameter",
parent: { parent: { model: [{ parameter: "C" }, { parameter: "C" }] } },
} as any;
expect(validator!.expression({ value: "C" } as any, twoRowsSettingC)).toBe(false);
expect(validator!.expression({ value: "kernel" } as any, twoRowsSettingC)).toBe(true);
expect(validator!.message(null, { formControl: { value: "C" } } as any)).toBe(
'"C" is already set by another row'
);
});

it("maps datasetVersionPath to the datasetversionselector field type", () => {
component.setFormlyFormBinding({
type: "object",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,24 @@ export class OperatorPropertyEditFrameComponent implements OnInit, OnChanges, On
};
}

// A field the schema marks unique holds a meaning the enclosing list cannot repeat.
// uniqueItems cannot say this: two hyperparameter rows naming one parameter differ in
// their other fields, so they are distinct items while still emitting one keyword twice.
if (mapSource.uniqueAmongRows === true) {
mappedField.validators.uniqueAmongRows = {
expression: (control: AbstractControl, field: FormlyFieldConfig) => {
const rows = field.parent?.parent?.model;
const key = field.key;
if (!isDefined(control?.value) || !Array.isArray(rows) || typeof key !== "string") {
return true;
}
return rows.filter(row => isDefined(row) && row[key] === control.value).length <= 1;
},
message: (error: any, field: FormlyFieldConfig) =>
`"${field.formControl?.value}" is already set by another row`,
};
}

// Add custom validators for attribute type
if (isDefined(mapSource.attributeTypeRules)) {
mappedField.validators.checkAttributeType = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,7 @@ export interface CustomJSONSchema7 extends JSONSchema7 {
hideOnNull?: boolean;

additionalEnumValue?: string;

// no two rows of the enclosing list may hold the same value for this field
uniqueAmongRows?: boolean;
}
Loading