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
6 changes: 3 additions & 3 deletions dist/js/Subworkflow.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -736,14 +736,14 @@ declare class Subworkflow extends InMemoryEntity implements SubworkflowSchema {
reason: string;
} | undefined;
findUnitKeyById(id: string): string;
private findUnitWithTag;
private findAssignmentUnitWithTag;
get hasConvergence(): boolean;
get convergenceParam(): string | undefined;
get convergenceResult(): string | undefined;
convergenceSeries(scopeTrack: JobSchema["scopeTrack"]): {
x: number;
param: any;
y: any;
param: unknown;
y: unknown;
}[];
updateMethodData(materials: Material[], metaProperties: MetaPropertyHolder[]): void;
addConvergence({ parameter, parameterInitial, parameterIncrement, result, resultInitial, condition, operator, tolerance, maxOccurrences, externalContext, }: ConvergenceConfig): void;
Expand Down
25 changes: 15 additions & 10 deletions dist/js/Subworkflow.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ class Subworkflow extends entity_1.InMemoryEntity {
const index = this.units.findIndex((u) => u.flowchartId === id);
return `units.${index}`;
}
findUnitWithTag(tag) {
findAssignmentUnitWithTag(tag) {
return this.units
.filter((unit) => unit.type === enums_1.UnitType.assignment)
.find((unit) => { var _a; return (_a = unit.tags) === null || _a === void 0 ? void 0 : _a.includes(tag); });
Expand All @@ -196,27 +196,32 @@ class Subworkflow extends entity_1.InMemoryEntity {
}
get convergenceParam() {
var _a;
return (_a = this.findUnitWithTag(enums_1.UnitTag.hasConvergenceParam)) === null || _a === void 0 ? void 0 : _a.operand;
return (_a = this.findAssignmentUnitWithTag(enums_1.UnitTag.hasConvergenceParam)) === null || _a === void 0 ? void 0 : _a.operand;
}
get convergenceResult() {
var _a;
return (_a = this.findUnitWithTag(enums_1.UnitTag.hasConvergenceResult)) === null || _a === void 0 ? void 0 : _a.operand;
return (_a = this.findAssignmentUnitWithTag(enums_1.UnitTag.hasConvergenceResult)) === null || _a === void 0 ? void 0 : _a.operand;
}
convergenceSeries(scopeTrack) {
if (!this.hasConvergence || !(scopeTrack === null || scopeTrack === void 0 ? void 0 : scopeTrack.length)) {
const { convergenceParam, convergenceResult } = this;
if (!convergenceParam || !convergenceResult || !(scopeTrack === null || scopeTrack === void 0 ? void 0 : scopeTrack.length)) {
return [];
}
let prevResult;
// `scopeTrack` stores per-repetition diffs: each item only carries the global/local keys
// that were added or changed in that repetition (see UnitEndpoint.saveUnitStatus). Accumulate
// the global scope across items so each iteration reads the full scope, not just its delta.
// This also stays correct for legacy full-snapshot scopeTrack data, since re-applying a full
// snapshot is idempotent.
const accumulatedGlobal = {};
return scopeTrack
.map((scopeItem, i) => {
var _a, _b;
var _a;
Object.assign(accumulatedGlobal, (_a = scopeItem.scope) === null || _a === void 0 ? void 0 : _a.global);
return {
x: i,
// TODO: fix types
// @ts-ignore
param: (_a = scopeItem.scope) === null || _a === void 0 ? void 0 : _a.global[this.convergenceParam],
// @ts-ignore
y: (_b = scopeItem.scope) === null || _b === void 0 ? void 0 : _b.global[this.convergenceResult],
param: accumulatedGlobal[convergenceParam],
y: accumulatedGlobal[convergenceResult],
};
})
.filter(({ y }) => {
Expand Down
24 changes: 15 additions & 9 deletions src/js/Subworkflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ class Subworkflow extends InMemoryEntity implements SubworkflowSchema {
return `units.${index}`;
}

private findUnitWithTag(tag: UnitTag) {
private findAssignmentUnitWithTag(tag: UnitTag) {
return this.units
.filter((unit) => unit.type === UnitType.assignment)
.find((unit) => unit.tags?.includes(tag));
Expand All @@ -290,29 +290,35 @@ class Subworkflow extends InMemoryEntity implements SubworkflowSchema {
}

get convergenceParam() {
return this.findUnitWithTag(UnitTag.hasConvergenceParam)?.operand;
return this.findAssignmentUnitWithTag(UnitTag.hasConvergenceParam)?.operand;
}

get convergenceResult() {
return this.findUnitWithTag(UnitTag.hasConvergenceResult)?.operand;
return this.findAssignmentUnitWithTag(UnitTag.hasConvergenceResult)?.operand;
}

convergenceSeries(scopeTrack: JobSchema["scopeTrack"]) {
if (!this.hasConvergence || !scopeTrack?.length) {
const { convergenceParam, convergenceResult } = this;

if (!convergenceParam || !convergenceResult || !scopeTrack?.length) {
return [];
}

let prevResult: unknown;
// `scopeTrack` stores per-repetition diffs: each item only carries the global/local keys
// that were added or changed in that repetition (see UnitEndpoint.saveUnitStatus). Accumulate
// the global scope across items so each iteration reads the full scope, not just its delta.
// This also stays correct for legacy full-snapshot scopeTrack data, since re-applying a full
// snapshot is idempotent.
const accumulatedGlobal: Record<string, unknown> = {};

return scopeTrack
.map((scopeItem, i) => {
Object.assign(accumulatedGlobal, scopeItem.scope?.global);
return {
x: i,
// TODO: fix types
// @ts-ignore
param: scopeItem.scope?.global[this.convergenceParam],
// @ts-ignore
y: scopeItem.scope?.global[this.convergenceResult],
param: accumulatedGlobal[convergenceParam],
y: accumulatedGlobal[convergenceResult],
};
})
.filter(({ y }) => {
Expand Down
100 changes: 100 additions & 0 deletions tests/js/Subworkflow.convergenceSeries.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import JSONSchemasInterface from "@mat3ra/esse/dist/js/esse/JSONSchemasInterface";
import esseSchemas from "@mat3ra/esse/dist/js/schemas.json";
import type { JobSchema } from "@mat3ra/esse/dist/js/types";
import { ApplicationRegistry } from "@mat3ra/standata";
import StandataDriver from "@mat3ra/standata/dist/js/StandataDriver";
import { expect } from "chai";
import type { JSONSchema7 } from "json-schema";

import { AssignmentUnit, Subworkflow } from "../../src/js";
import { UnitTag } from "../../src/js/enums";

const CONVERGENCE_PARAM = "N_k";
const CONVERGENCE_RESULT = "total_energy";

function buildConvergenceSubworkflow({ withConvergence = true } = {}) {
const units = withConvergence
? [
new AssignmentUnit({
name: "set_kpoints",
flowchartId: "set-kpoints",
operand: CONVERGENCE_PARAM,
value: "12",
tags: [UnitTag.hasConvergenceParam],
}).toJSON(),
new AssignmentUnit({
name: "save_energy",
flowchartId: "save-energy",
operand: CONVERGENCE_RESULT,
value: "total_energy",
tags: [UnitTag.hasConvergenceResult],
}).toJSON(),
]
: [];
return new Subworkflow({ ...Subworkflow.defaultConfig, units });
}

function makeScopeItem(
global: Record<string, unknown>,
): NonNullable<JobSchema["scopeTrack"]>[number] {
return { scope: { global, local: {} } };
}

describe("Subworkflow.convergenceSeries", () => {
before(() => {
JSONSchemasInterface.setSchemas(esseSchemas as JSONSchema7[]);
ApplicationRegistry.setDriver(new StandataDriver());
});

it("returns [] when the subworkflow has no convergence units", () => {
const subworkflow = buildConvergenceSubworkflow({ withConvergence: false });
const scopeTrack = [makeScopeItem({ [CONVERGENCE_PARAM]: 12, [CONVERGENCE_RESULT]: -100 })];

expect(subworkflow.convergenceSeries(scopeTrack)).to.deep.equal([]);
});

it("returns [] for empty/undefined scopeTrack", () => {
const subworkflow = buildConvergenceSubworkflow();

expect(subworkflow.convergenceSeries([])).to.deep.equal([]);
expect(subworkflow.convergenceSeries(undefined)).to.deep.equal([]);
});

it("reconstructs the full scope from per-repetition diffs (param carried over from earlier diff)", () => {
const subworkflow = buildConvergenceSubworkflow();

// Diff-only scopeTrack: each item carries only keys added/changed in that repetition.
// The last item omits `total_energy` (unchanged) so the param must come from accumulation.
const scopeTrack = [
makeScopeItem({ [CONVERGENCE_PARAM]: 12, [CONVERGENCE_RESULT]: -100 }),
makeScopeItem({ [CONVERGENCE_PARAM]: 24, [CONVERGENCE_RESULT]: -98 }),
makeScopeItem({ [CONVERGENCE_PARAM]: 36 }),
];

const series = subworkflow.convergenceSeries(scopeTrack);

// Last repetition is dropped because the (accumulated) result did not change.
expect(series).to.deep.equal([
{ x: 1, param: 12, y: -100 },
{ x: 2, param: 24, y: -98 },
]);
});

it("matches the diff-based output for legacy full-snapshot scopeTrack (back-compat)", () => {
const subworkflow = buildConvergenceSubworkflow();

// Legacy format: every item stores the full cumulative scope.
const scopeTrack = [
makeScopeItem({ [CONVERGENCE_PARAM]: 12, [CONVERGENCE_RESULT]: -100 }),
makeScopeItem({ [CONVERGENCE_PARAM]: 24, [CONVERGENCE_RESULT]: -98 }),
makeScopeItem({ [CONVERGENCE_PARAM]: 36, [CONVERGENCE_RESULT]: -98 }),
];

const series = subworkflow.convergenceSeries(scopeTrack);

expect(series).to.deep.equal([
{ x: 1, param: 12, y: -100 },
{ x: 2, param: 24, y: -98 },
]);
});
});
Loading