Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Running a python model instead #100

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
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
96 changes: 96 additions & 0 deletions apps/class-solid/public/python/bmi_class.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# class BmiClass:
# def __init__(self):
# self.config = {}
# self.model = None
# print("CLASS instantiated!")

# def initialize(self, config: dict):
# # from class_model import CLASS

# self.config = config
# self.model = CLASS(config)
# print("CLASS initialized!")

# def update(self):
# self.model.update()

# def get_component_name(self):
# return "Chemistry Land-surface Atmosphere Soil Slab model"

# def get_output_item_count(self):
# return len(self.get_output_var_names())

# def get_output_var_names(self):
# return ["h", "theta", "dtheta", "q", "dq"]

# def get_var_grid(self, name: str):
# return 1

# def get_var_type(self, name: str):
# return "float"

# def get_var_location(self, name: str):
# return "node"

# def get_current_time(self):
# return self.model.t

# def get_end_time(self):
# return self.config["timeControl"]["runtime"]

# def get_time_units(self):
# return "s"

# def get_time_step(self):
# return self.config["timeControl"]["dt"]

# def get_value(self, name: str):
# if name in self.get_output_var_names():
# return [getattr(self.model, name)]
# raise ValueError(f"Variable {name} not found")

# def get_grid_type(self):
# return "scalar"

# def run(self, freq=600, var_names=None):
# if var_names is None:
# var_names = self.get_output_var_names()

# output = {"t": []}
# for name in var_names:
# output[name] = []

# while self.model.t <= self.config["timeControl"]["runtime"]:
# if self.model.t % freq == 0:
# output["t"].append(self.model.t)
# for name in var_names:
# output[name].append(getattr(self.model, name))
# self.update()

# return output

class SubClass:
def __init__(self):
self.value = 42

class BmiClass:
def __init__(self):
print("BmiClass in python instantiated")
self.model = None

def get_component_name(self):
return "Chemistry Land-surface Atmosphere Soil Slab model"

def initialize(self, cfg: dict):
# Instantiate another class (SubClass) here
self.model = SubClass()

def get_value(self, var: str):
return self.model.value

def get_output_var_names(self):
return ["h", "theta", "dtheta", "q", "dq"]

def run(self, var_names=["t"]):
output = {"t": []}
return output
119 changes: 119 additions & 0 deletions apps/class-solid/public/python/class_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Constants
RHO = 1.2 # Density of air [kg m-3]
CP = 1005.0 # Specific heat of dry air [J kg-1 K-1]


class CLASS:
"""
CLASS model definition.

Attributes:
_cfg: Object containing the model settings.
h: ABL height [m].
theta: Mixed-layer potential temperature [K].
dtheta: Temperature jump at h [K].
q: Mixed-layer specific humidity [kg kg-1].
dq: Specific humidity jump at h [kg kg-1].
t: Model time [s].
"""

def __init__(self, config: dict):
"""
Create object and initialize the model state.

Args:
config: Model settings as a dictionary.
"""
self._cfg = config
self.h = config["initialState"]["h_0"]
self.theta = config["initialState"]["theta_0"]
self.dtheta = config["initialState"]["dtheta_0"]
self.q = config["initialState"]["q_0"]
self.dq = config["initialState"]["dq_0"]
self.t = 0

def update(self):
"""Integrate mixed layer."""
dt = self._cfg["timeControl"]["dt"]
self.h += dt * self.htend
self.theta += dt * self.thetatend
self.dtheta += dt * self.dthetatend
self.q += dt * self.qtend
self.dq += dt * self.dqtend
self.t += dt

@property
def htend(self) -> float:
"""Tendency of CLB [m s-1]."""
return self.we + self.ws

@property
def thetatend(self) -> float:
"""Tendency of mixed-layer potential temperature [K s-1]."""
return (self._cfg["mixedLayer"]["wtheta"] - self.wthetae) / self.h + self._cfg[
"mixedLayer"
]["advtheta"]

@property
def dthetatend(self) -> float:
"""Tendency of potential temperature jump at h [K s-1]."""
w_th_ft = 0.0 # TODO: add free troposphere switch
return (
self._cfg["mixedLayer"]["gammatheta"] * self.we - self.thetatend + w_th_ft
)

@property
def qtend(self) -> float:
"""Tendency of mixed-layer specific humidity [kg kg-1 s-1]."""
return (self._cfg["mixedLayer"]["wq"] - self.wqe) / self.h + self._cfg[
"mixedLayer"
]["advq"]

@property
def dqtend(self) -> float:
"""Tendency of specific humidity jump at h [kg kg-1 s-1]."""
w_q_ft = 0 # TODO: add free troposphere switch
return self._cfg["mixedLayer"]["gammaq"] * self.we - self.qtend + w_q_ft

@property
def we(self) -> float:
"""Entrainment velocity [m s-1]."""
we = -self.wthetave / self.dthetav

# Don't allow boundary layer shrinking
return max(we, 0)

@property
def ws(self) -> float:
"""Large-scale vertical velocity [m s-1]."""
return -self._cfg["mixedLayer"]["divU"] * self.h

@property
def wthetae(self) -> float:
"""Entrainment kinematic heat flux [K m s-1]."""
return -self.we * self.dtheta

@property
def wqe(self) -> float:
"""Entrainment moisture flux [kg kg-1 m s-1]."""
return -self.we * self.dq

@property
def wthetave(self) -> float:
"""Entrainment kinematic virtual heat flux [K m s-1]."""
return -self._cfg["mixedLayer"]["beta"] * self.wthetav

@property
def dthetav(self) -> float:
"""Virtual temperature jump at h [K]."""
return (self.theta + self.dtheta) * (
1.0 + 0.61 * (self.q + self.dq)
) - self.theta * (1.0 + 0.61 * self.q)

@property
def wthetav(self) -> float:
"""Surface kinematic virtual heat flux [K m s-1]."""
return (
self._cfg["mixedLayer"]["wtheta"]
+ 0.61 * self.theta * self._cfg["mixedLayer"]["wq"]
)
12 changes: 10 additions & 2 deletions apps/class-solid/src/lib/runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,18 @@ const worker = new Worker(new URL("./worker.ts", import.meta.url), {
});
export const AsyncBmiClass = wrap<typeof BmiClass>(worker);

export async function runClass(config: PartialConfig): Promise<ClassOutput> {
const pyodideWorker = new Worker(new URL("./worker_pyodide.ts", import.meta.url), {
type: "module",
});
export const PyodideClass = wrap<typeof BmiClass>(pyodideWorker)

export async function runClass(config: PartialConfig, backend='pyodide'): Promise<ClassOutput> {
try {
const parsedConfig: Config = parse(config);
const model = await new AsyncBmiClass();
const model = backend === 'ts'
? await new AsyncBmiClass()
: await new PyodideClass();
console.log(await model.get_component_name())
await model.initialize(parsedConfig);
const output = await model.run({
var_names: new BmiClass().get_output_var_names(),
Expand Down
112 changes: 112 additions & 0 deletions apps/class-solid/src/lib/worker_pyodide.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { loadPyodide } from "https://cdn.jsdelivr.net/pyodide/v0.27.0/full/pyodide.mjs";
import type { BmiClass } from "@classmodel/class/bmi";
import { expose } from "comlink";


const bmiClassCode = `
class SubClass:
def __init__(self):
self.value = 42

class BmiClass:
def __init__(self):
print("BmiClass in python instantiated")
self.model = None

def get_component_name(self):
return "Chemistry Land-surface Atmosphere Soil Slab model"

def initialize(self, cfg: dict):
# Instantiate another class (SubClass) here
self.model = SubClass()

def get_value(self, var: str):
return self.model.value

def get_output_var_names(self):
return ["h", "theta", "dtheta", "q", "dq"]

def run(self, var_names=["t"]):
output = {"t": [10, 11, 12]}
return output

import json
model = BmiClass()
`

class BmiWrapper implements BmiClass {
private pyodide: any;

Check failure on line 38 in apps/class-solid/src/lib/worker_pyodide.ts

View workflow job for this annotation

GitHub Actions / quality

lint/suspicious/noExplicitAny

Unexpected any. Specify a different type.
private bmiInstance: any;

Check failure on line 39 in apps/class-solid/src/lib/worker_pyodide.ts

View workflow job for this annotation

GitHub Actions / quality

lint/suspicious/noExplicitAny

Unexpected any. Specify a different type.

constructor() {
// Use the globally initialized pyodide
if (typeof pyodide === 'undefined') {
throw new Error("Pyodide is not loaded yet.");
}

this.pyodide = pyodide; // Access the global pyodide instance

// Load the Python BMI class code (this should only be done once)
this.pyodide.runPython(bmiClassCode);
console.log("Python BMI loaded");

// Get the Python instance of the model
this.bmiInstance = this.pyodide.globals.get('model');

// Ensure that the model is initialized and has the expected methods
if (!this.bmiInstance) {
throw new Error("Failed to initialize Python BMI model.");
}
}

get_component_name(): string {
return this.pyodide.runPython('model.get_component_name()');
}

initialize(cfg: object): void {
this.pyodide.runPython(`model.initialize(${JSON.stringify(cfg)})`);
}

get_value(varName: string): [number] {
return this.pyodide.runPython(`model.get_value(${JSON.stringify(varName)})`);
}

get_output_var_names(): string[] {
return this.pyodide.runPython('model.get_output_var_names()');
}

run<T extends string[]>({ freq, var_names }: { freq?: number; var_names?: T }): { t: number[] } & { [K in T[number]]: number[] } {
const varNames = JSON.stringify(var_names || ['t']);
const result = this.pyodide.runPython(`json.dumps(model.run(var_names=${varNames}))`)
const jsResult = JSON.parse((result))
console.log(result, jsResult)
return jsResult
}
}

// Global pyodide instance
let pyodide: any;

Check failure on line 88 in apps/class-solid/src/lib/worker_pyodide.ts

View workflow job for this annotation

GitHub Actions / quality

lint/suspicious/noExplicitAny

Unexpected any. Specify a different type.

async function initializeWorker() {
try {
// Load Pyodide globally (this is done only once)
pyodide = await loadPyodide();
console.log("Pyodide initialized!");

// Create the BMI wrapper (no need to pass pyodide here)
const model = new BmiWrapper();
console.log(model.get_component_name());
model.initialize({ a: 10 });
console.log(model.get_value('random_var'));
const output = model.run({ var_names: ['t'] });
console.log(output);
console.log(typeof output);

expose(BmiWrapper); // Expose the BmiWrapper class to the main thread if needed

} catch (error) {
console.error("Error initializing worker:", error);
}
}

initializeWorker();
Loading