"""
TransientBVD - Transducer Model
This module defines the `Transducer` class, which represents an ultrasound
transducer with predefined parameters. The transducer stores electrical circuit
parameters and additional metadata such as the manufacturer and resonance frequency.
"""
import json
import math
import os
from dataclasses import dataclass
from importlib import resources
from pathlib import Path
from typing import Dict, Optional, Any, Mapping, Union
[docs]
@dataclass
class Transducer:
"""
Represents an ultrasound transducer described by a BVD equivalent circuit.
Parameters
----------
rs : float
Series resistance in ohms. Must be positive.
ls : float
Inductance in henries. Must be positive.
cs : float
Series capacitance in farads. Must be positive.
c0 : float
Parallel capacitance in farads. Must be positive.
rp : Optional[float], optional
Parallel resistance in ohms. If provided, must be positive.
name : str, optional
Human-readable transducer name.
manufacturer : Optional[str], optional
Manufacturer name.
"""
rs: float
ls: float
cs: float
c0: float
rp: Optional[float] = None
name: str = "Unknown"
manufacturer: Optional[str] = None
def __post_init__(self):
"""
Validate parameters upon initialization.
"""
self._validate_parameters()
def _validate_parameters(self) -> None:
"""
Ensure all electrical parameters are positive.
Raises
------
ValueError
If any parameter is non-positive.
"""
if self.rs <= 0:
raise ValueError("Series resistance (rs) must be positive.")
if self.ls <= 0:
raise ValueError("Inductance (ls) must be positive.")
if self.cs <= 0:
raise ValueError("Series capacitance (cs) must be positive.")
if self.c0 <= 0:
raise ValueError("Parallel capacitance (c0) must be positive.")
if self.rp is not None and self.rp <= 0:
raise ValueError("Parallel resistance (rp) must be positive if provided.")
@property
def frequency(self) -> float:
"""
Compute the resonance frequency dynamically.
Returns
-------
float
Resonance frequency in Hz.
"""
return 1 / (2 * math.pi * (self.ls * self.cs) ** 0.5)
[docs]
def set_name(self, name: str) -> "Transducer":
"""
Set the transducer's name.
Parameters
----------
name : str
The name to assign to the transducer.
Returns
-------
Transducer
The updated transducer instance (supports method chaining).
"""
self.name = name
return self
[docs]
def set_manufacturer(self, manufacturer: str) -> "Transducer":
"""
Set the manufacturer of the transducer.
Parameters
----------
manufacturer : str
The manufacturer name.
Returns
-------
Transducer
The updated transducer instance (supports method chaining).
"""
self.manufacturer = manufacturer
return self
[docs]
def set_rp(self, rp: float) -> "Transducer":
"""
Set the parallel resistance (`rp`) value.
Parameters
----------
rp : float
Parallel resistance in ohms. Must be positive.
Returns
-------
Transducer
The updated transducer instance (supports method chaining).
Raises
------
ValueError
If `rp` is not positive.
"""
if rp <= 0:
raise ValueError("Parallel resistance (rp) must be positive.")
self.rp = rp
return self
def __str__(self) -> str:
"""
Return a user-friendly string representation of the transducer.
Returns
-------
str
A formatted string with transducer details.
"""
return (
f"Transducer: {self.name}\n"
f"Manufacturer: {self.manufacturer or 'Unknown'}\n"
f"Rs={self.rs:.4f} Ω, Ls={self.ls:.6f} H, Cs={self.cs:.2e} F, C0={self.c0:.2e} F\n"
f"Resonance Frequency: {self.frequency:.2f} Hz\n"
f"Parallel Resistance (Rp): {self.rp if self.rp else 'None'} Ω"
)
# Environment variable that can override the transducer database location
TRANSDUCERS_ENV_VAR = "TRANSIENTBVD_TRANSDUCERS_JSON"
# Relative path inside the package for the bundled default database
DEFAULT_TRANSDUCERS_RESOURCE = "data/transducers.json"
JsonPath = Union[str, Path]
def _load_transducer_db_json(json_file: Optional[JsonPath] = None) -> Mapping[str, Any]:
"""
Load the transducer database JSON using a clean precedence order:
1) Explicit `json_file` argument (path on disk)
2) Environment variable TRANSIENTBVD_TRANSDUCERS_JSON (path on disk)
3) Bundled default resource inside the installed package
This works for normal installs and wheels because it uses importlib.resources
for the bundled default.
"""
# 1) explicit path wins
if json_file is not None:
json_path = Path(json_file)
with json_path.open("r", encoding="utf-8") as f:
return json.load(f)
# 2) environment override
env_path = os.environ.get(TRANSDUCERS_ENV_VAR)
if env_path:
json_path = Path(env_path)
with json_path.open("r", encoding="utf-8") as f:
return json.load(f)
# 3) bundled default (package data)
# Note: resources.files(...) is available in Python 3.9+
resource = resources.files("transientbvd").joinpath(DEFAULT_TRANSDUCERS_RESOURCE)
with resource.open("r", encoding="utf-8") as f:
return json.load(f)
[docs]
def load_transducers(json_file: Optional[JsonPath] = None) -> Dict[str, Transducer]:
"""
Load transducer data and create Transducer objects.
Parameters
----------
json_file : Optional[Union[str, Path]]
Path to a JSON file. If None:
- will try env var TRANSIENTBVD_TRANSDUCERS_JSON
- otherwise loads the bundled default database.
Returns
-------
Dict[str, Transducer]
Dictionary keyed by transducer name.
"""
data = _load_transducer_db_json(json_file=json_file)
transducers: Dict[str, Transducer] = {}
for name, params in data.items():
t = (
Transducer(
rs=float(params["rs"]),
ls=float(params["ls"]),
cs=float(params["cs"]),
c0=float(params["c0"]),
rp=float(params["rp"]) if params.get("rp") is not None else None,
)
.set_name(str(name))
.set_manufacturer(params.get("manufacturer", "Unknown"))
)
transducers[str(name)] = t
return transducers
[docs]
def load_measured_transducers(
json_file: Optional[JsonPath] = None,
) -> Dict[str, Transducer]:
"""
Backwards-compatible alias for load_transducers().
"""
return load_transducers(json_file=json_file)
[docs]
def select_transducer(name: str, json_file: Optional[JsonPath] = None) -> Transducer:
"""
Retrieve a predefined transducer by its name.
Parameters
----------
name : str
Name of the transducer to retrieve.
json_file : Optional[Union[str, Path]]
Optional JSON database path override.
Raises
------
ValueError
If the transducer name does not exist.
"""
measured_transducers = load_measured_transducers(json_file=json_file)
if name not in measured_transducers:
available = ", ".join(measured_transducers.keys())
raise ValueError(
f"Transducer '{name}' not found. Available transducers: {available}"
)
return measured_transducers[name]
[docs]
def predefined_transducers(
json_file: Optional[JsonPath] = None,
) -> Dict[str, Transducer]:
"""
Get a dictionary of all predefined transducers.
Parameters
----------
json_file : Optional[Union[str, Path]]
Optional JSON database path override.
Returns
-------
Dict[str, Transducer]
Dictionary mapping transducer names to Transducer objects.
"""
return load_measured_transducers(json_file=json_file)