Skip to content

Conversation

@interim17
Copy link
Contributor

@interim17 interim17 commented Apr 21, 2025

Time Estimate or Size

medium-large

Problem

Final round of controller refactoring of controller and simulator params.

@meganrm @toloudis The approach to most of these changes was discussed in detail at our meeting a few weeks back. I officially futzed with this for too long and then went on vacation. Simularium is paused, would be good to get this done but I know it's not high priority.

More than half of the lines of changes are new tests or moved utils, so the line count isn't as bad as it seems.

After this I will finalize prefetching and controller speed with these new changes and that will get me close to putting simularium work to bed for now.

The utilities that are shared by the BinaryFIleReader and SimulariumController tests I moved to a util file.

Solution

We now have typed interfaces for the argument passed to changeFile, there is no more code path to initialize the controller with params. I've updated the readme to show that changeFile needs to be called imperatively or in a lifecycle method in order to load a trajectory.

I added quite a bit of testing to the controller, where previously there wasn't much. There could be more, but the goal was to add tests relevant to this change set, and establish a base of coverage so that in the future when we add features, adding tests doesn't require as big of alift.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
  • This change requires updated or new tests

Comment on lines 257 to 275
///// File Changing /////

private clearFileResources(reuseSimulator = false): void {
this.closeConversionConnection();
this.stop();
this.isFileChanging = false;
if (!reuseSimulator) {
this.simulator = undefined;
}
this.playBackFile = "";
this.visData.clearForNewTrajectory();
this.simulator?.abort();
this.pause();
if (this.visGeometry) {
this.visGeometry.clearForNewTrajectory();
this.visGeometry.resetCamera();
}
}

public handleFileChange(
simulariumFile: ISimulariumFile,
fileName: string,
geoAssets?: { [key: string]: string }
): Promise<FileReturn> {
if (!fileName.includes(".simularium")) {
throw new Error("File must be a .simularium file");
}

if (geoAssets) {
return this.changeFile({ simulariumFile, geoAssets }, fileName);
} else {
return this.changeFile({ simulariumFile }, fileName);
}
}

public cancelCurrentFile(newFileName: string): void {
this.isFileChanging = true;
this.playBackFile = newFileName;

// calls simulator.abort()
this.stop();

this.visData.WaitForFrame(0);
this.visData.clearForNewTrajectory();
public clearFile(): void {
this.isFileChanging = false;
this.clearFileResources();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

clearFile as a part of the public API is not functionally changed, but the business logic has been moved into clearFileResources to allow some internal flexibility when we want to switch between remote files and reuse the simulator.

Comment on lines 19 to 42
export const getSimulatorClassFromParams = (params?: SimulatorParams) => {
if (!params || !params.fileName) {
return { simulatorClass: null, typedParams: null };
}
if ("netConnectionSettings" in params) {
return {
simulatorClass: RemoteSimulator,
typedParams: params as RemoteSimulatorParams,
};
} else if ("clientSimulatorImpl" in params) {
return {
simulatorClass: ClientSimulator,
typedParams: params as ClientSimulatorParams,
};
} else if ("simulariumFile" in params) {
return {
simulatorClass: LocalFileSimulator,
typedParams: params as LocalFileSimulatorParams,
};
} else {
return { simulatorClass: null, typedParams: null };
}
};

Copy link
Contributor Author

Choose a reason for hiding this comment

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

getSimulatorClassFromParams handles generating the right type of simulator for the incoming params, the simulators have runtime checks to make sure they have received the right params as this can be run in JS on the client side. @meganrm outlined this function in comments on previous PR

Comment on lines +65 to +74
const mockConversionClient = {
setOnConversionCompleteHandler: vi.fn().mockImplementation(() => {
console.log("mocked handler");
}),
cancelConversion: vi.fn(),
disconnect: vi.fn(),
convertTrajectory: vi.fn().mockResolvedValue(undefined),
sendSmoldynData: vi.fn().mockResolvedValue(undefined),
lastRequestedFile: "",
};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This doesn't mock the internal functionality of the conversion client or the messages we expect to receive, but rather the presence of the functions and if they get called at the right time. Internal testing out of scope imo.

Comment on lines 81 to 96
vi.mock("../Simulator/ISimulator", async () => {
const actual = await vi.importActual<
typeof import("../Simulator/ISimulator")
>("../Simulator/ISimulator");

return {
...actual,
getSimulatorClassFromParams: (params?: SimulatorParams) => {
const result = actual.getSimulatorClassFromParams(params);
if (result.simulatorClass === RemoteSimulator) {
return { ...result, simulatorClass: DummyRemoteSimulator };
}
return result;
},
};
});
Copy link
Contributor Author

@interim17 interim17 May 6, 2025

Choose a reason for hiding this comment

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

This may seem a bit convoluted and I'm open to suggestions, but what we are achieving here is getSimulatorClassFromParams returns a dummy remote simulator.

A reason to do this is that by removing the old controller params, we lost the code path to let the client provide the simulator they want to use, and so we need a way to get the dummy in play for testing. This is bit of a hack, but keeps the mess here in the testing and out of the production code.

@interim17 interim17 marked this pull request as ready for review May 12, 2025 17:50
@interim17 interim17 requested a review from a team as a code owner May 12, 2025 17:50
@interim17 interim17 requested review from frasercl, meganrm and toloudis and removed request for a team and frasercl May 12, 2025 17:50
} else {
return { simulatorClass: null, typedParams: null };
}
};
Copy link
Contributor

Choose a reason for hiding this comment

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

this seems to be the wrong place to put this code. ISimulator should not depend on the files that implement ISimulator.

Also do you think this function could probably be more like a factory method that actually returns a ISimulator instance (call new ClientSimulator, or new RemoteSimulator etc, inside the if statements)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@meganrm want to weigh in on this? I left this where and as you wrote it for the most part.

import { ISimulator } from "../simularium/ISimulator.js";
import { LocalFileSimulator } from "../simularium/LocalFileSimulator.js";
import {
getSimulatorClassFromParams,
Copy link
Contributor

Choose a reason for hiding this comment

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

prob could just define getSimulatorClassFromParams in here, or make a new ts module called SimulatorFactory.ts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moved into SimulatorFactory.ts

"netConnectionSettings" in params &&
this.simulator instanceof RemoteSimulator &&
this.simulator.isConnectedToRemoteServer() &&
this.lastNetConnectionConfig === params.netConnectionSettings
Copy link
Contributor

Choose a reason for hiding this comment

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

I like this function, but does this need to be a deeper equality check to compare the netConnectionSettings?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed to ==

Copy link
Contributor

@toloudis toloudis Jun 9, 2025

Choose a reason for hiding this comment

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

that (changing from === to ==) would be shallower... deeper check would be to compare the equality of the actual properties of the object. (technically double equals is not "shallower" but is less strict)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ha, I misunderstood your comment

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Pushed a more robust check

@interim17 interim17 requested a review from toloudis May 27, 2025 17:41
prefetchFrames?: boolean;
}

export interface ClientSimulatorParams extends BaseSimulatorParams {
Copy link
Contributor

Choose a reason for hiding this comment

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

just curious, why do these all extend BaseSimulatorParams? it feels like BaseSimulatorParams is never going to grow beyond fileName and ClientSimulatorParams doesn't even have a fileName..
Is BaseSimulatorParams used anywhere outside of this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe I'm misunderstanding you, but ClientSimulatorParams does have a "file name" in a sense. If you look at the way the test bed loads client simulators we use strings in the ID field to serve as file name, and the downstream simulator factory method and initialize calls expect all incoming params to have a value for that field.

It's true that you currently can't actually load a client simulator file from your local machine (like via drag and drop, although that feature should exist imho) but even the "file name" for a remote trajectory is more of an ID. I think we could have something saved on octopus with a random string and as long as the server sent the right data back it's just a convention that the remote files have a .simularium extension.

I'd have to poke around but it seems like a pretty baked-in convention/assumption that everything that gets loaded in the viewer has a file name, would take some refactoring in a few places to take it out.

From test bed configs:

    {
        id: "blood-plasma-1.0.simularium",
        name: "Blood Plasma",
        simulatorType: SimulatorModes.remotePrecomputed,
    },
    {
        id: "TEST_POINTS",
        name: "Point Simulation",
        simulatorType: SimulatorModes.localClientSimulator,
    },
    ```
As for the base class we could just make each param interface have fileName rather than use a base class though, I don't remember why we did that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe trajectoryId or simulationId would be more general than filename then. I think my comment was partly motivated because it seemed odd to introduce a base class for just one single variable.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok, pushed a quick change removing the base class but retaining fileName.

If you'd like to also see the move towards a different naming I'll work on that as well, but that naming convention is a bit more spread out so might be more involved.

@interim17 interim17 requested a review from toloudis July 9, 2025 17:02
public clearPendingFile() {
this.setState({ filePending: null });
}

Copy link
Contributor

Choose a reason for hiding this comment

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

quick sanity check: this looks like something important, where the code wanted to know that there was a file in-flight? is it really ok to remove it and within scope of this PR? or is this dead code that was never used?

Copy link
Contributor

@toloudis toloudis left a comment

Choose a reason for hiding this comment

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

I think this is looking good. Only concern now, is there more work after merging this to update simularium-website? 😢 And mucho testing with existing production simulations from the server!

LocalFileSimulatorParams,
} from "./types";

export const getSimulatorClassFromParams = (params?: SimulatorParams) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: just for the record, a true "factory" would actually CREATE and return instances of these classes. Just for clarity of terminology, as this is sort of a meta-factory which is just finding out what kind of class to create.

}

export interface ClientSimulatorParams {
fileName: string;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think my comments around this code earlier were based on thinking that for a ClientSimulator, this string is not really a file name.

Copy link
Contributor

Choose a reason for hiding this comment

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

is there already a PR in simularium-website that accounts for the interface changes in here?

// export interface NetConnectionParams {
// serverIp?: string;
// serverPort?: number;
// }
Copy link
Contributor

Choose a reason for hiding this comment

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

can delete now!

newConfig?.serverIp != null &&
newConfig?.serverPort != null &&
lastConfig.serverIp === newConfig.serverIp &&
lastConfig.serverPort === newConfig.serverPort
Copy link
Contributor

Choose a reason for hiding this comment

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

if you used lastConfig?.serverPort a couple lines up, why not use ?. here?

Copy link
Contributor

Choose a reason for hiding this comment

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

can we make this shouldReuseSimulator testable to demonstrate it returns true or false for the conditions we care about?... (edit) I found the tests but they are higher level. would be cool if we could actually unit test just this function but i am basically satisfied for this.

@meganrm
Copy link
Contributor

meganrm commented Aug 4, 2025

I have read through this and looks good, but I still want to test it with the education app.

Could be of interest: One thing I've noticed using the main branch is the "wait for frame" is never really used because the cache is cleared so often the waited for frame gets reset before the frames arrive

@interim17
Copy link
Contributor Author

interim17 commented Aug 4, 2025

I have read through this and looks good, but I still want to test it with the education app.

Could be of interest: One thing I've noticed using the main branch is the "wait for frame" is never really used because the cache is cleared so often the waited for frame gets reset before the frames arrive

I'll check that out. It's all getting a little foggy, but in some branch related to prefetching I have a callback based version of waitForFrame that is more robust. Could also try to make sure we aren't running it when its not needed.

Comment on lines +301 to +307
if (
"simulariumFile" in params &&
!params.fileName.includes(".simularium")
) {
this.handleError("File must be a .simularium file");
return Promise.reject({ status: FILE_STATUS_FAIL });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm trying to use this for the binding affinity app. For local files, simulariumFile is the implementation, not a filename, so it's not going to include a .simularium extension.

Copy link
Contributor

Choose a reason for hiding this comment

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

I removed these lines and the file loaded. For context, this is how I call changeFile:

 const response = await fetch(url);
        if (response.ok) {
            const blob = await response.blob();
            const simulariumFile = await loadSimulariumFile(blob);
            await simulariumController.changeFile({
                fileName: "example",
                simulariumFile: simulariumFile,
            });
            const plotData = simulariumFile.getPlotData();
            setTrajectoryPlotData(plotData[0].data as ScatterTrace[]); // we're not using histograms
        } else {
            throw new Error(`Failed to fetch - ${response.status}`);
        }

@meganrm
Copy link
Contributor

meganrm commented Aug 5, 2025

if you haven't already, I think you should make a branch on the website repo that installs this version and make sure every way of loading a file works: url, local, drag and drop local, examples etc

@toloudis
Copy link
Contributor

toloudis commented Aug 5, 2025

if you haven't already, I think you should make a branch on the website repo that installs this version and make sure every way of loading a file works: url, local, drag and drop local, examples etc

YES. test all the things.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants