Function for calculating value based on variable input

I’m working on an application where the user selects an option from a drop-down, which gets captured as a variable [$UserSelection].

From there, a function would query a dataset for all entries with a value in the dataset that matches $UserSelection. The function would then group those results by like values in ColumnX and then find the average of the values each group has ColumnY, weighted for their respective values in ColumnZ.

I had what I think was a 90% solution with Python, but I was running into some issues and had to switch to TypeScript. I’ve run into more issues, but I’m not as familiar with the language and am unsure what is attributable to my limited understanding of the language versus that of Palantir. I was wondering if someone could point me in the right direction or toward some more useful documentation.

The process would look something like this:

@Catch can you clarify what you’re using to create this application? Workshop? Slate? Custom code via the OSDK?

Of course, my apologies.

I’m building the app in Workshop and am attempting to create the function in Code Repository. The intent is to reference the product of the function as a variable in a dashboard. The idea is to show the current situation by executing one version of the function with the as-is data and then show what could be by altering the ColumnY figure based on the user’s selection.

Got it, that makes sense. I think you’re in need of a custom aggregation, but I also want to clarify – are you querying datasets or objects in the ontology?

It’s objects in the ontology. The nomenclature is killing me.

I’ll have to look at custom aggregation. I hacked together the partial solution in the code below, but the “calculateAllPotentialCons” function produces a JSON and I somehow can’t figure out how to reasonably bring that into Workshop. I would have expected it to be easily made into a table, but that doesn’t seem to be the case.

import { Function, Double } from "@foundry/functions-api";
import { Objects } from "@foundry/ontology-api";

export class ConFunctions {
    @Function()
    public calculateEquipCond(EquipName: string): Double {
        return this.calculateCon(EquipName, false, "", "");
    }

    @Function()
    public calculatePotentialEquipCond(EquipName: string, ProjectSelectedPart: string, SelectedProjectWorkItemID: string): Double {
        return this.calculateCon(EquipName, true, ProjectSelectedPart, SelectedProjectWorkItemID);
    }

    @Function()
    public calculateAllPotentialCons(EquipName: string): string {
        const matchedEquipments = Objects.search()
            .mwaa()
            .filter(mwaa => mwaa.bldgName.exactMatch(EquipName))
            .all();

        const PartsCons = new Map<string, [number, number, number]>(); 

        const currentCon = this.calculateEquipCond(EquipName);

        const uniqueParts = new Set(matchedEquipments.map(Equipment => Equipment.Part));
        uniqueParts.forEach(Part => {
            if (Part) { 
                const PartEquipment = matchedEquipments.find(Equipment => Equipment.Part === Part);
                const estimatedCost = PartEquipment && PartEquipment.estimatedCost !== undefined ? PartEquipment.estimatedCost : 0;
                const potentialCon = this.calculateCon(EquipName, true, Part, "");
                PartsCons.set(Part, [currentCon, potentialCon, estimatedCost]);
            }
        });

        const results = Array.from(PartsCons).map(([Part, Cons]) => ({
            Part,
            'Current Con': Cons[0],
            'Potential Con': Cons[1],
            'Cost-Benefit Evaluation': Cons[2] ? (Cons[1] - Cons[0]) / Cons[2] : 'N/A'
        }));

        return JSON.stringify(results); 
    }

    private calculateCon(EquipName: string, isPotential: boolean, selectedPart: string, selectedWorkItemID: string): Double {
        const matchedEquipments = Objects.search()
            .mwaa()
            .filter(mwaa => mwaa.bldgName.exactMatch(EquipName))
            .all();

        let totalWeightedCon = 0;
        let totalCost = 0;

        for (const Equipment of matchedEquipments) {
            if (Equipment.Part && Equipment.PartCon && Equipment.PartCost && Equipment.bldgName) {
                let PartCon = Equipment.PartCon;
                if (isPotential && Equipment.Part === selectedPart && (selectedWorkItemID === "" || Equipment.workItemId === selectedWorkItemID)) {
                    PartCon = 100;
                }

                totalCost += Equipment.PartCost;
                totalWeightedCon += PartCon * Equipment.PartCost;
            }
        }

        return totalCost > 0 ? totalWeightedCon / totalCost : 0; 
    }
}

Hey @Catch!

You’re right in that Workshop has a way to easily bring the output of TypeScript functions into the frontend, but it may require a little refactoring of your calculateAllPotentialCons function.

Depending on exactly how you want to display the aggregated data, there are two options you can look into:

Function-backed Properties/Column(s) in Existing Object Table
If you’re already displaying the data in an existing Object Table widget, you can write a function that returns and renders one or more new columns. Please see the documentation linked above; note that you may have to create a custom type schema depending on what you want to return:

interface CalculatedProperties {
    current_con: Double,
    potential_con: Double,
    cost_benefit_evaluation: Double
}

which you can then reference in the return type of your function header:

@Function()
public calculateAllPotentialCons(EquipName: string): FunctionsMap<(ObjectTypeInObjectTableWidget), CalculatedProperties> {
    // ...
}

New Object Set in new Object Table
Alternatively, if you wanted to display the aggregated data in a new Object Table widget, you can create a function that returns an Object Set that you can store as a separate variable in your Workshop application. You can then use that new variable as the input to a separate Object Table.

Your function header would look similar to this:

@Function()
public calculateAllPotentialCons(EquipName: string): ObjectType[] {
    const varName: ObjectType[] = Objects.search().ObjectTypeAPIName().all();
    // additional filtering and aggregating logic here
    return varName;
}
1 Like

Thank you for the insight, @josh. I’m now failing in new and exciting ways.

I ended up with this code:

import { Function, Double } from "@foundry/functions-api";
import { Objects } from "@foundry/ontology-api";

export interface CalculatedProperties {
    currentBCI: Double,
    potentialBCI: Double,
    estimatedCost: Double
}

export class BciFunctions {

    @Function()
    public async calculateAllPotentialBCIs(BuildingName: string): Promise<string> { // Note the return type is now String
    const matchedBuildings = await Objects.search().xxxxx().filter(xxxxx => xxxxx.bldgName.exactMatch(BuildingName)).all();
    let result: CalculatedProperties[] = [];

        const currentBCI = this.calculateBuildingBCI(BuildingName);

        const uniqueSystems = new Set(matchedBuildings.map(building => building.system));
        for (const system of uniqueSystems) {
            if (system) {
                const systemBuilding = matchedBuildings.find(building => building.system === system);
                const estimatedCost = systemBuilding && systemBuilding.estimatedCost !== undefined ? systemBuilding.estimatedCost : 0;
                const potentialBCI = this.calculateBCI(BuildingName, true, system, "");
                result.push({ currentBCI, potentialBCI, estimatedCost });
        }
    }

    return JSON.stringify(result);
    }

    private calculateBCI(BuildingName: string, isPotential: boolean, selectedSystem: string, selectedWorkItemID: string): number {
        const matchedBuildings = Objects.search()
            .xxxxx()
            .filter(xxxxx => xxxxx.bldgName.exactMatch(BuildingName))
            .all();

        let totalWeightedCI = 0;
        let totalCRV = 0;

        for (const building of matchedBuildings) {
            if (building.system && building.systemCi && building.systemCrv && building.bldgName) {
                let systemCI = building.systemCi;
                if (isPotential && building.system === selectedSystem && (selectedWorkItemID === "" || building.workItemId === selectedWorkItemID)) {
                    systemCI = 100;
                }

                totalCRV += building.systemCrv;
                totalWeightedCI += systemCI * building.systemCrv;
            }
        }

        return totalCRV > 0 ? totalWeightedCI / totalCRV : 0;
    }
}

and ended up with the following error message when I attempted to load an object table:


#### Failed to load objects for table

Something went wrong while fetching object property values. Please refresh the page to try again.

[Error] Unsupported function return type: customType

Hey @Catch,

The return type of Promise< string> will not work in this case since Object Tables in Workshop can display the data a function returns if the output is either an Object Set (subset of all objects of the same Object Type) or a FunctionsMap.

1. Returned Object Set
If you want to display just a subset of objects (let’s say we’re working with a ‘Buildings’ Object Type), both the return type in the function header and the actual variable returned should be of the Building type, i.e.:

@Function()
public async calculateAllPotentialBCIs(BuildingName: string): Building[] {
    const desiredBuildings = Building[];
    // logic to filter to final object set

    return desiredBuildings;
}

Note that when returning an object set, Workshop essentially recognizes the output as a list of primary keys for the included objects and renders it accordingly.

2. Returned FunctionsMap
If you’re trying to add new columns to an existing object table widget, your function header and the output variable can be written like so (where ‘xxxxx’ is the Object Type displayed in the table):

@Function()
public async calculateAllPotentialBCIs(BuildingName: string): FunctionsMap<xxxxx, CalculatedProperties> {
    const result = new FunctionsMap<xxxxx, CalculatedProperties>();
    // additional logic here
    for (const system of uniqueSystems) {
        // additional logic here
        result.set(system, {
            currentBCI: a,
            potentialBCI: b,
            estimatedCost: c
        }
    }
}

Workshop recognizes the FunctionsMap output as something similar to a dictionary/map in python where the keys in each key-value pair are the primary keys of the objects, and the pairs are a nested dictionary of your custom type. When Workshop renders the function’s output, it uses the key-value pair to locate the object/row in your table, and then adds the respective values for each metric in your custom type under N (in your case, 3) new columns.

1 Like

Hey, Josh, I’m still trying to work through this, but I wanted to say thank you for the help. Hopefully I’ll be able to come back with a solid solution in a bit.

1 Like

I finally got it, mostly, sorted!

I ended up using both of your suggestions. First, I created a function that returned an object set that was a subset of the main one. Once I confirmed that new object set was accessible in an object table, I created a second function that added new columns to that table.

1 Like