diff --git a/cmd/new-ui/v1beta1/main.go b/cmd/new-ui/v1beta1/main.go
index 1566dfca864..6c94e1605b3 100644
--- a/cmd/new-ui/v1beta1/main.go
+++ b/cmd/new-ui/v1beta1/main.go
@@ -59,6 +59,7 @@ func main() {
http.HandleFunc("/katib/fetch_hp_job_info/", kuh.FetchHPJobInfo)
http.HandleFunc("/katib/fetch_hp_job_trial_info/", kuh.FetchHPJobTrialInfo)
+ http.HandleFunc("/katib/fetch_hp_job_label_info/", kuh.FetchHPJobLabelInfo)
http.HandleFunc("/katib/fetch_nas_job_info/", kuh.FetchNASJobInfo)
http.HandleFunc("/katib/fetch_trial_templates/", kuh.FetchTrialTemplates)
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/constants/algorithms-settings.const.ts b/pkg/new-ui/v1beta1/frontend/src/app/constants/algorithms-settings.const.ts
index 0ca7f3c0b90..b34b2b8c01b 100644
--- a/pkg/new-ui/v1beta1/frontend/src/app/constants/algorithms-settings.const.ts
+++ b/pkg/new-ui/v1beta1/frontend/src/app/constants/algorithms-settings.const.ts
@@ -242,6 +242,29 @@ export const DartsSettings: AlgorithmSetting[] = [
},
];
+export const PbtSettings: AlgorithmSetting[] = [
+ {
+ name: 'suggestion_trial_dir',
+ value: '/var/log/katib/checkpoints/',
+ type: AlgorithmSettingType.STRING,
+ },
+ {
+ name: 'n_population',
+ value: 40,
+ type: AlgorithmSettingType.INTEGER,
+ },
+ {
+ name: 'resample_probability',
+ value: null,
+ type: AlgorithmSettingType.FLOAT,
+ },
+ {
+ name: 'truncation_threshold',
+ value: 0.2,
+ type: AlgorithmSettingType.FLOAT,
+ },
+];
+
export const EarlyStoppingSettings: AlgorithmSetting[] = [
{
name: 'min_trials_required',
@@ -271,4 +294,5 @@ export const AlgorithmSettingsMap: { [key: string]: AlgorithmSetting[] } = {
[AlgorithmsEnum.SOBOL]: SOBOLSettings,
[AlgorithmsEnum.ENAS]: ENASSettings,
[AlgorithmsEnum.DARTS]: DartsSettings,
+ [AlgorithmsEnum.PBT]: PbtSettings,
};
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/constants/algorithms-types.const.ts b/pkg/new-ui/v1beta1/frontend/src/app/constants/algorithms-types.const.ts
index 607e6f2db63..4f1f0aa0cfd 100644
--- a/pkg/new-ui/v1beta1/frontend/src/app/constants/algorithms-types.const.ts
+++ b/pkg/new-ui/v1beta1/frontend/src/app/constants/algorithms-types.const.ts
@@ -12,6 +12,7 @@ export const AlgorithmNames = {
[AlgorithmsEnum.MULTIVARIATE_TPE]: 'Multivariate Tree of Parzen Estimators',
[AlgorithmsEnum.CMAES]: 'Covariance Matrix Adaptation: Evolution Strategy',
[AlgorithmsEnum.SOBOL]: 'Sobol Quasirandom Sequence',
+ [AlgorithmsEnum.PBT]: 'Population Based Training',
};
export const NasAlgorithmNames = {
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/enumerations/algorithms.enum.ts b/pkg/new-ui/v1beta1/frontend/src/app/enumerations/algorithms.enum.ts
index 425d423199d..8f7476468b1 100644
--- a/pkg/new-ui/v1beta1/frontend/src/app/enumerations/algorithms.enum.ts
+++ b/pkg/new-ui/v1beta1/frontend/src/app/enumerations/algorithms.enum.ts
@@ -9,6 +9,7 @@ export enum AlgorithmsEnum {
SOBOL = 'sobol',
ENAS = 'enas',
DARTS = 'darts',
+ PBT = 'pbt',
}
export enum EarlyStoppingAlgorithmsEnum {
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.html b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.html
index 4d1233302cf..e650ccee888 100644
--- a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.html
+++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.html
@@ -60,6 +60,13 @@
[experimentJson]="experimentDetails"
>
+
+
+
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.ts b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.ts
index 33f129b07c7..f4e4889f5d4 100644
--- a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.ts
+++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.component.ts
@@ -30,6 +30,7 @@ export class ExperimentDetailsComponent implements OnInit, OnDestroy {
columns: string[] = [];
details: string[][] = [];
experimentTrialsCsv: string;
+ labelCsv: string;
hoveredTrial: number;
experimentDetails: ExperimentK8s;
showGraph: boolean;
@@ -94,6 +95,11 @@ export class ExperimentDetailsComponent implements OnInit, OnDestroy {
this.details = this.parseTrialsDetails(data.details);
this.showGraph = true;
});
+ this.backendService
+ .getExperimentLabelInfo(this.name, this.namespace)
+ .subscribe(response => {
+ this.labelCsv = response;
+ });
this.backendService
.getExperiment(this.name, this.namespace)
.subscribe((response: ExperimentK8s) => {
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.module.ts b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.module.ts
index 541edb0af97..4a8983364f8 100644
--- a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.module.ts
+++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/experiment-details.module.ts
@@ -16,6 +16,7 @@ import { ExperimentOverviewModule } from './overview/experiment-overview.module'
import { ExperimentDetailsTabModule } from './details/experiment-details-tab.module';
import { TrialsGraphModule } from './trials-graph/trials-graph.module';
import { ExperimentYamlModule } from './yaml/experiment-yaml.module';
+import { PbtTabModule } from './pbt/pbt-tab-loader.module';
@NgModule({
declarations: [ExperimentDetailsComponent],
@@ -33,6 +34,7 @@ import { ExperimentYamlModule } from './yaml/experiment-yaml.module';
MatProgressSpinnerModule,
ExperimentYamlModule,
TitleActionsToolbarModule,
+ PbtTabModule,
],
exports: [ExperimentDetailsComponent],
})
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab-loader.module.ts b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab-loader.module.ts
new file mode 100644
index 00000000000..0b45f713e29
--- /dev/null
+++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab-loader.module.ts
@@ -0,0 +1,22 @@
+import { NgModule } from '@angular/core';
+import { CommonModule } from '@angular/common';
+
+import { FormsModule } from '@angular/forms';
+import { MatFormFieldModule } from '@angular/material/form-field';
+import { MatSelectModule } from '@angular/material/select';
+import { MatCheckboxModule } from '@angular/material/checkbox';
+
+import { PbtTabComponent } from './pbt-tab.component';
+
+@NgModule({
+ declarations: [PbtTabComponent],
+ imports: [
+ CommonModule,
+ FormsModule,
+ MatFormFieldModule,
+ MatSelectModule,
+ MatCheckboxModule,
+ ],
+ exports: [PbtTabComponent],
+})
+export class PbtTabModule {}
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab.component.html b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab.component.html
new file mode 100644
index 00000000000..f9627740ca0
--- /dev/null
+++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab.component.html
@@ -0,0 +1,21 @@
+
+
+
+ Y-Axis
+
+
+ {{ name }}
+
+
+
+
+ Display Seed Traces
+
+
+
+
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab.component.scss b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab.component.scss
new file mode 100644
index 00000000000..47bac1bf984
--- /dev/null
+++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab.component.scss
@@ -0,0 +1,48 @@
+:host {
+ display: block;
+}
+
+.pbt-wrapper {
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex-direction: column;
+}
+
+.pbt-options-wrapper {
+ display: flex;
+ align-items: center;
+ flex-direction: row;
+}
+
+.pbt-option {
+ margin: 10px;
+}
+
+.d3-tab-graph {
+ width: 400px;
+ text-align: center;
+
+ @media (min-width: 768px) {
+ width: 700px;
+ }
+
+ @media (min-width: 1024px) {
+ width: 1000px;
+ }
+
+ @media (min-width: 1400px) {
+ width: 1300px;
+ }
+
+ @media (min-width: 1650px) {
+ width: 1600px;
+ }
+
+ @media (min-width: 2000px) {
+ width: 1900px;
+ }
+
+ height: 600px;
+}
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab.component.ts b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab.component.ts
new file mode 100644
index 00000000000..c2f6172f37b
--- /dev/null
+++ b/pkg/new-ui/v1beta1/frontend/src/app/pages/experiment-details/pbt/pbt-tab.component.ts
@@ -0,0 +1,463 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ Input,
+ OnChanges,
+ OnInit,
+ AfterViewInit,
+ SimpleChanges,
+ ElementRef,
+ ViewChild,
+} from '@angular/core';
+import lowerCase from 'lodash-es/lowerCase';
+import { safeDivision } from 'src/app/shared/utils';
+import { ExperimentK8s } from '../../../models/experiment.k8s.model';
+
+import { Inject } from '@angular/core';
+import { DOCUMENT } from '@angular/common';
+
+import { StatusEnum } from 'src/app/enumerations/status.enum';
+
+declare let d3: any;
+
+type PbtPoint = {
+ trialName: string;
+ parentUid: string;
+ parameters: Object; // all y-axis possible values (parameters + alternativeMetrics)
+ generation: number; // generation
+ metricValue: number; // evaluation metric
+};
+
+@Component({
+ selector: 'app-experiment-pbt-tab',
+ templateUrl: './pbt-tab.component.html',
+ styleUrls: ['./pbt-tab.component.scss'],
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class PbtTabComponent implements OnChanges, OnInit, AfterViewInit {
+ @ViewChild('pbtGraph') graphWrapper: ElementRef; // used to manipulate svg dom
+
+ graph: any; // svg dom
+ selectableNames: string[]; // list of parameters/metrics for UI dropdown
+ selectedName: string; // user selected parameter/metric
+ displayTrace: boolean;
+
+ private labelData: { [trialName: string]: PbtPoint } = {};
+ private trialData: { [trialName: string]: Object } = {};
+ private parameterNames: string[]; // parameter names
+ private goalName: string = '';
+ private data: PbtPoint[][] = []; // data sorted by generation and segment
+ private graphHelper: any; // graph metadata and auxiliary info
+
+ @Input()
+ experiment: ExperimentK8s;
+
+ @Input()
+ labelCsv: string[] = [];
+
+ @Input()
+ experimentTrialsCsv: string[] = [];
+
+ constructor(@Inject(DOCUMENT) private document: Document) {
+ this.graphHelper = {};
+ this.graphHelper.margin = { top: 10, right: 30, bottom: 30, left: 60 };
+ this.graphHelper.width =
+ 460 - this.graphHelper.margin.left - this.graphHelper.margin.right;
+ this.graphHelper.height =
+ 400 - this.graphHelper.margin.top - this.graphHelper.margin.bottom;
+ // Track angular initialization since using @Input from template
+ this.graphHelper.ngInit = false;
+ }
+
+ ngOnInit(): void {
+ if (this.experiment.spec.algorithm.algorithmName != 'pbt') {
+ // Prevent initialization if not Pbt
+ return;
+ }
+
+ // Create full list of parameters
+ this.parameterNames = this.experiment.spec.parameters.map(param => {
+ return param.name;
+ });
+ // Identify goal
+ this.goalName = this.experiment.spec.objective.objectiveMetricName;
+ // Create full list of selectable names
+ this.selectableNames = [...this.parameterNames];
+ if (
+ this.experiment.spec.objective.additionalMetricNames &&
+ this.experiment.spec.objective.additionalMetricNames.length > 0
+ ) {
+ this.selectableNames = [
+ ...this.selectableNames,
+ ...this.experiment.spec.objective.additionalMetricNames,
+ ];
+ }
+ // Create converters for all possible y-axes
+ this.graphHelper.yMeta = {};
+ for (const param of this.experiment.spec.parameters) {
+ this.graphHelper.yMeta[param.name] = {};
+ if (
+ param.parameterType == 'discrete' ||
+ param.parameterType == 'categorical'
+ ) {
+ this.graphHelper.yMeta[param.name].transform = x => {
+ return x;
+ };
+ this.graphHelper.yMeta[param.name].isNumber = false;
+ } else if (param.parameterType == 'double') {
+ this.graphHelper.yMeta[param.name].transform = x => {
+ return parseFloat(x);
+ };
+ this.graphHelper.yMeta[param.name].isNumber = true;
+ } else {
+ this.graphHelper.yMeta[param.name].transform = x => {
+ return parseInt(x);
+ };
+ this.graphHelper.yMeta[param.name].isNumber = true;
+ }
+ }
+ if (this.experiment.spec.objective.additionalMetricNames) {
+ for (const metricName of this.experiment.spec.objective
+ .additionalMetricNames) {
+ if (this.graphHelper.yMeta.hasOwnProperty(metricName)) {
+ console.warn(
+ 'Additional metric name conflict with parameter name; ignoring metric:',
+ metricName,
+ );
+ continue;
+ }
+ this.graphHelper.yMeta[metricName] = {};
+ this.graphHelper.yMeta[metricName].transform = x => {
+ return parseFloat(x);
+ };
+ this.graphHelper.yMeta[metricName].isNumber = true;
+ }
+ }
+
+ this.graphHelper.ngInit = true;
+ }
+
+ ngAfterViewInit(): void {
+ if (this.experiment.spec.algorithm.algorithmName != 'pbt') {
+ // Remove pbt tab and tab content
+ const tabs = document.querySelectorAll('.mat-tab-labels .mat-tab-label');
+ for (let i = 0; i < tabs.length; i++) {
+ if (
+ tabs[i]
+ .querySelector('.mat-tab-label-content')
+ .innerHTML.includes('PBT')
+ ) {
+ const tabId = tabs[i].getAttribute('id');
+ const tabBodyId = tabId.replace('label', 'content');
+ const tabBody = document.querySelector('#' + tabBodyId);
+ tabBody.remove();
+ tabs[i].remove();
+ break;
+ }
+ }
+ return;
+ }
+ // Specify default choice for dropdown menu
+ this.selectedName = this.selectableNames[0];
+ // Specify default trace view
+ this.displayTrace = false;
+ }
+
+ onDropdownChange() {
+ // Trigger graph redraw on dropdown change event
+ this.clearGraph();
+ this.updateGraph();
+ }
+
+ onTraceChange() {
+ // Trigger graph redraw on trace change event
+ // TODO: could use d3.select(..).remove() instead of recreating
+ this.clearGraph();
+ this.updateGraph();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (!this.graphHelper || !this.graphHelper.ngInit) {
+ console.warn(
+ 'graphHelper not initialized yet, attempting manual call to ngOnInit()',
+ );
+ this.ngOnInit();
+ }
+ // Recompute formatted plotting points on data input changes
+ let updatePoints = false;
+ if (changes.experimentTrialsCsv && this.experimentTrialsCsv) {
+ let trialArr = d3.csv.parse(this.experimentTrialsCsv);
+ for (let trial of trialArr) {
+ if (
+ trial['Status'] == StatusEnum.SUCCEEDED &&
+ !this.trialData.hasOwnProperty(trial['trialName'])
+ ) {
+ this.trialData[trial['trialName']] = trial;
+ updatePoints = true;
+ }
+ }
+ }
+
+ if (changes.labelCsv && this.labelCsv) {
+ let labelArr = d3.csv.parse(this.labelCsv);
+ for (let label of labelArr) {
+ if (this.labelData.hasOwnProperty(label['trialName'])) {
+ continue;
+ }
+ let newEntry: PbtPoint = {
+ trialName: label['trialName'],
+ parentUid: label['pbt.suggestion.katib.kubeflow.org/parent'],
+ generation: parseInt(
+ label['pbt.suggestion.katib.kubeflow.org/generation'],
+ ),
+ parameters: undefined,
+ metricValue: undefined,
+ };
+ this.labelData[newEntry.trialName] = newEntry;
+ updatePoints = true;
+ }
+ }
+
+ if (updatePoints) {
+ // Lazy; reprocess all points
+ let points: PbtPoint[] = [];
+ Object.values(this.labelData).forEach(entry => {
+ let point = {} as PbtPoint;
+ point.trialName = entry.trialName;
+ point.generation = entry.generation;
+ point.parentUid = entry.parentUid;
+
+ // Find corresponding trial data
+ let trial = this.trialData[point.trialName];
+ if (trial !== undefined) {
+ point.metricValue = parseFloat(trial[this.goalName]);
+ point.parameters = {};
+ for (let p of this.selectableNames) {
+ point.parameters[p] = this.graphHelper.yMeta[p].transform(trial[p]);
+ }
+ } else {
+ point.metricValue = undefined;
+ }
+ points.push(point);
+ });
+
+ // Generate segments
+ // Group seeds
+ let remaining_points = {};
+ for (let p of points) {
+ remaining_points[p.trialName] = p;
+ }
+
+ this.data = [];
+ while (Object.keys(remaining_points).length > 0) {
+ let seeds = this.maxGenerationTrials(remaining_points);
+ for (let seed of seeds) {
+ let segment = [];
+ let v = seed;
+ while (v) {
+ segment.push(v);
+ let delete_entry = v.trialName;
+ v = remaining_points[v.parentUid];
+ delete remaining_points[delete_entry];
+ }
+ this.data.push(segment);
+ }
+ }
+
+ this.clearGraph();
+ this.updateGraph();
+ }
+ }
+
+ private maxGenerationTrials(d) {
+ let end_seeds = [];
+ let max_generation = 0;
+ for (let k of Object.keys(d)) {
+ if (d[k].generation > max_generation) {
+ max_generation = d[k].generation;
+ end_seeds = [];
+ end_seeds.push(d[k]);
+ } else if (d[k].generation === max_generation) {
+ end_seeds.push(d[k]);
+ }
+ }
+ return end_seeds;
+ }
+
+ private clearGraph() {
+ // Clear any existing views from graphs object
+ if (this.graph) {
+ // this.graph.remove(); // d3 remove from DOM
+ d3.select(this.graphWrapper.nativeElement).select('svg').remove();
+ }
+ this.graph = undefined;
+ }
+
+ private createGraph() {
+ this.graph = d3
+ .select(this.graphWrapper.nativeElement)
+ .append('svg')
+ .attr(
+ 'width',
+ this.graphHelper.width +
+ this.graphHelper.margin.left +
+ this.graphHelper.margin.right,
+ )
+ .attr(
+ 'height',
+ this.graphHelper.height +
+ this.graphHelper.margin.top +
+ this.graphHelper.margin.bottom,
+ )
+ .append('g')
+ .attr(
+ 'transform',
+ 'translate(' +
+ this.graphHelper.margin.left +
+ ',' +
+ this.graphHelper.margin.top +
+ ')',
+ );
+ }
+
+ private getRangeX() {
+ const xValues = Object.values(this.labelData).map(entry => {
+ return entry.generation;
+ });
+ return d3.scale
+ .linear()
+ .domain(d3.extent(xValues))
+ .range([0, this.graphHelper.width]);
+ }
+
+ private getRangeY(key) {
+ if (this.selectableNames.includes(key)) {
+ const paramValues = Object.keys(this.trialData).map(trialName => {
+ return this.graphHelper.yMeta[key].transform(
+ this.trialData[trialName][key],
+ );
+ });
+
+ if (this.graphHelper.yMeta[key].isNumber) {
+ return d3.scale
+ .linear()
+ .domain(d3.extent(paramValues))
+ .range([this.graphHelper.height, 0]);
+ } else {
+ paramValues.sort((a, b) => a - b);
+ return d3.scale
+ .ordinal()
+ .domain(paramValues)
+ .range([this.graphHelper.height, 0]);
+ }
+ } else {
+ console.error('Key(' + key + ') not found in y-axis list');
+ }
+ }
+
+ private getColorScaleZ() {
+ let values = [];
+ for (const segment of this.data) {
+ for (const point of segment) {
+ values.push(point.metricValue);
+ }
+ }
+ return d3.scale
+ .linear()
+ .domain(d3.extent(values))
+ .interpolate(d3.interpolateHcl)
+ .range([d3.rgb('#cfd8dc'), d3.rgb('#263238')]);
+ }
+
+ private updateGraph() {
+ if (!this.graphHelper || !this.graphHelper.ngInit) {
+ // ngOnInit not called yet
+ return;
+ }
+ if (!this.data.length || this.data.length == 0) {
+ // Data not initialized
+ return;
+ }
+ if (!this.graph) {
+ this.createGraph();
+ }
+
+ // Add X axis
+ let xAxis = this.getRangeX();
+ this.graph
+ .append('g')
+ .attr('transform', 'translate(0,' + this.graphHelper.height + ')')
+ .call(d3.svg.axis().scale(xAxis).orient('bottom'));
+ this.graph
+ .append('text')
+ .attr('text-anchor', 'middle')
+ .attr('x', this.graphHelper.width / 2)
+ .attr('y', this.graphHelper.height + 30)
+ .text('Generation');
+ // Add Y axis
+ let yAxis = this.getRangeY(this.selectedName);
+ this.graph.append('g').call(d3.svg.axis().scale(yAxis).orient('left'));
+ this.graph
+ .append('text')
+ .attr('text-anchor', 'middle')
+ .attr('x', -this.graphHelper.height / 2)
+ .attr('y', -50)
+ .attr('transform', 'rotate(-90)')
+ .text(this.selectedName);
+ // Change line width
+ this.graph
+ .selectAll('path')
+ .style({ stroke: 'black', fill: 'none', 'stroke-width': '1px' });
+
+ // Add the points
+ const sparam = this.selectedName;
+ const colorScale = this.getColorScaleZ();
+ for (const segment of this.data) {
+ // Plot only valid points
+ const validSegment = segment.filter(
+ point =>
+ point.parameters &&
+ point.parameters[sparam] &&
+ point.metricValue !== undefined,
+ );
+ this.graph
+ .append('g')
+ .selectAll('dot')
+ .data(validSegment)
+ .enter()
+ .append('circle')
+ .attr('cx', function (d) {
+ return xAxis(d.generation);
+ })
+ .attr('cy', function (d) {
+ return yAxis(d.parameters[sparam]);
+ })
+ .attr('r', 2)
+ .attr('fill', function (d) {
+ return colorScale(d.metricValue);
+ });
+
+ if (this.displayTrace && validSegment.length > 0) {
+ // Add the lines
+ const strokeColor = colorScale(validSegment[0].metricValue);
+ this.graph
+ .append('path')
+ .datum(validSegment)
+ .attr('fill', 'none')
+ .attr('stroke', strokeColor)
+ .attr('stroke-width', 1)
+ .attr(
+ 'd',
+ d3.svg
+ .line()
+ .x(function (d) {
+ return xAxis(d.generation);
+ })
+ .y(function (d) {
+ return yAxis(d.parameters[sparam]);
+ }),
+ );
+ }
+ }
+ }
+}
diff --git a/pkg/new-ui/v1beta1/frontend/src/app/services/backend.service.ts b/pkg/new-ui/v1beta1/frontend/src/app/services/backend.service.ts
index 60ac1ab0c34..458d26b17e5 100644
--- a/pkg/new-ui/v1beta1/frontend/src/app/services/backend.service.ts
+++ b/pkg/new-ui/v1beta1/frontend/src/app/services/backend.service.ts
@@ -56,6 +56,12 @@ export class KWABackendService extends BackendService {
return this.http.get(url).pipe(catchError(error => this.parseError(error)));
}
+ getExperimentLabelInfo(name: string, namespace: string): Observable {
+ const url = `/katib/fetch_hp_job_label_info/?experimentName=${name}&namespace=${namespace}`;
+
+ return this.http.get(url).pipe(catchError(error => this.parseError(error)));
+ }
+
getExperiment(name: string, namespace: string): Observable {
const url = `/katib/fetch_experiment/?experimentName=${name}&namespace=${namespace}`;
diff --git a/pkg/new-ui/v1beta1/hp.go b/pkg/new-ui/v1beta1/hp.go
index 006d799c78d..9f3fb7a8502 100644
--- a/pkg/new-ui/v1beta1/hp.go
+++ b/pkg/new-ui/v1beta1/hp.go
@@ -245,3 +245,63 @@ func (k *KatibUIHandler) FetchHPJobTrialInfo(w http.ResponseWriter, r *http.Requ
return
}
}
+
+func (k *KatibUIHandler) FetchHPJobLabelInfo(w http.ResponseWriter, r *http.Request) {
+ //enableCors(&w)
+ experimentName := r.URL.Query()["experimentName"][0]
+ namespace := r.URL.Query()["namespace"][0]
+
+ trialList, err := k.katibClient.GetTrialList(experimentName, namespace)
+ if err != nil {
+ log.Printf("GetTrialList from HP job failed: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ log.Printf("Got Trial List")
+
+ labelList := map[string]int{}
+ labelList["trialName"] = 0
+ var trialRes [][]string
+ for _, t := range trialList.Items {
+ trialResText := make([]string, len(labelList))
+ trialResText[labelList["trialName"]] = t.Name
+ for k, v := range t.ObjectMeta.Labels {
+ i, exists := labelList[k]
+ if !exists {
+ i = len(labelList)
+ labelList[k] = i
+ trialResText = append(trialResText, "")
+ }
+ trialResText[i] = v
+ }
+ trialRes = append(trialRes, trialResText)
+ }
+
+ // Format header output
+ headerArr := make([]string, len(labelList))
+ for k, v := range labelList {
+ headerArr[v] = k
+ }
+ resultText := strings.Join(headerArr, ",")
+
+ // Format entry output
+ for _, row := range trialRes {
+ resultText += "\n" + strings.Join(row, ",")
+ for j := 0; j < len(labelList)-len(row); j++ {
+ resultText += ","
+ }
+ }
+
+ log.Printf("Logs parsed, results:\n %v", resultText)
+ response, err := json.Marshal(resultText)
+ if err != nil {
+ log.Printf("Marshal result text for HP job failed: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+ if _, err = w.Write(response); err != nil {
+ log.Printf("Write result text for HP job failed: %v", err)
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+}